Skip to content

Commit ccfd1fa

Browse files
authored
Merge branch 'main' into benibenj/scattered-sailfish
2 parents cbea9a0 + b4d980c commit ccfd1fa

File tree

27 files changed

+668
-94
lines changed

27 files changed

+668
-94
lines changed

src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ export interface IHighlight {
2222
}
2323

2424
export interface IHighlightedLabelOptions {
25-
26-
/**
27-
* Whether the label supports rendering icons.
28-
*/
29-
readonly supportIcons?: boolean;
30-
3125
readonly hoverDelegate?: IHoverDelegate;
3226
}
3327

@@ -41,7 +35,6 @@ export class HighlightedLabel extends Disposable {
4135
private text: string = '';
4236
private title: string = '';
4337
private highlights: readonly IHighlight[] = [];
44-
private supportIcons: boolean;
4538
private didEverRender: boolean = false;
4639
private customHover: IManagedHover | undefined;
4740

@@ -53,7 +46,6 @@ export class HighlightedLabel extends Disposable {
5346
constructor(container: HTMLElement, private readonly options?: IHighlightedLabelOptions) {
5447
super();
5548

56-
this.supportIcons = options?.supportIcons ?? false;
5749
this.domNode = dom.append(container, dom.$('span.monaco-highlighted-label'));
5850
}
5951

@@ -73,7 +65,7 @@ export class HighlightedLabel extends Disposable {
7365
* @param escapeNewLines Whether to escape new lines.
7466
* @returns
7567
*/
76-
set(text: string | undefined, highlights: readonly IHighlight[] = [], title: string = '', escapeNewLines?: boolean) {
68+
set(text: string | undefined, highlights: readonly IHighlight[] = [], title: string = '', escapeNewLines?: boolean, supportIcons?: boolean) {
7769
if (!text) {
7870
text = '';
7971
}
@@ -90,10 +82,10 @@ export class HighlightedLabel extends Disposable {
9082
this.text = text;
9183
this.title = title;
9284
this.highlights = highlights;
93-
this.render();
85+
this.render(supportIcons);
9486
}
9587

96-
private render(): void {
88+
private render(supportIcons?: boolean): void {
9789

9890
const children: Array<HTMLSpanElement | string> = [];
9991
let pos = 0;
@@ -105,7 +97,7 @@ export class HighlightedLabel extends Disposable {
10597

10698
if (pos < highlight.start) {
10799
const substring = this.text.substring(pos, highlight.start);
108-
if (this.supportIcons) {
100+
if (supportIcons) {
109101
children.push(...renderLabelWithIcons(substring));
110102
} else {
111103
children.push(substring);
@@ -114,7 +106,7 @@ export class HighlightedLabel extends Disposable {
114106
}
115107

116108
const substring = this.text.substring(pos, highlight.end);
117-
const element = dom.$('span.highlight', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring]);
109+
const element = dom.$('span.highlight', undefined, ...supportIcons ? renderLabelWithIcons(substring) : [substring]);
118110

119111
if (highlight.extraClasses) {
120112
element.classList.add(...highlight.extraClasses);
@@ -126,7 +118,7 @@ export class HighlightedLabel extends Disposable {
126118

127119
if (pos < this.text.length) {
128120
const substring = this.text.substring(pos,);
129-
if (this.supportIcons) {
121+
if (supportIcons) {
130122
children.push(...renderLabelWithIcons(substring));
131123
} else {
132124
children.push(substring);

src/vs/base/browser/ui/iconLabel/iconLabel.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface IIconLabelValueOptions {
3131
suffix?: string;
3232
hideIcon?: boolean;
3333
extraClasses?: readonly string[];
34+
bold?: boolean;
3435
italic?: boolean;
3536
strikethrough?: boolean;
3637
matches?: readonly IMatch[];
@@ -40,6 +41,7 @@ export interface IIconLabelValueOptions {
4041
readonly separator?: string;
4142
readonly domId?: string;
4243
iconPath?: URI;
44+
supportIcons?: boolean;
4345
}
4446

4547
class FastLabelNode {
@@ -136,6 +138,10 @@ export class IconLabel extends Disposable {
136138
labelClasses.push(...options.extraClasses);
137139
}
138140

141+
if (options.bold) {
142+
labelClasses.push('bold');
143+
}
144+
139145
if (options.italic) {
140146
labelClasses.push('italic');
141147
}
@@ -185,7 +191,7 @@ export class IconLabel extends Disposable {
185191
if (description || this.descriptionNode) {
186192
const descriptionNode = this.getOrCreateDescriptionNode();
187193
if (descriptionNode instanceof HighlightedLabel) {
188-
descriptionNode.set(description || '', options ? options.descriptionMatches : undefined, undefined, options?.labelEscapeNewLines);
194+
descriptionNode.set(description || '', options ? options.descriptionMatches : undefined, undefined, options?.labelEscapeNewLines, options?.supportIcons);
189195
this.setupHover(descriptionNode.element, options?.descriptionTitle);
190196
} else {
191197
descriptionNode.textContent = description && options?.labelEscapeNewLines ? HighlightedLabel.escapeNewLines(description, []) : (description || '');
@@ -247,7 +253,7 @@ export class IconLabel extends Disposable {
247253
if (!this.descriptionNode) {
248254
const descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container'))));
249255
if (this.creationOptions?.supportDescriptionHighlights) {
250-
this.descriptionNode = this._register(new HighlightedLabel(dom.append(descriptionContainer.element, dom.$('span.label-description')), { supportIcons: !!this.creationOptions.supportIcons }));
256+
this.descriptionNode = this._register(new HighlightedLabel(dom.append(descriptionContainer.element, dom.$('span.label-description'))));
251257
} else {
252258
this.descriptionNode = this._register(new FastLabelNode(dom.append(descriptionContainer.element, dom.$('span.label-description'))));
253259
}
@@ -338,14 +344,17 @@ class LabelWithHighlights extends Disposable {
338344
this.label = label;
339345
this.options = options;
340346

347+
// Determine supportIcons: use option if provided, otherwise use constructor value
348+
const supportIcons = options?.supportIcons ?? this.supportIcons;
349+
341350
if (typeof label === 'string') {
342351
if (!this.singleLabel) {
343352
this.container.textContent = '';
344353
this.container.classList.remove('multiple');
345-
this.singleLabel = this._register(new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), { supportIcons: this.supportIcons }));
354+
this.singleLabel = this._register(new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId }))));
346355
}
347356

348-
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines);
357+
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines, supportIcons);
349358
} else {
350359
this.container.textContent = '';
351360
this.container.classList.add('multiple');
@@ -360,8 +369,8 @@ class LabelWithHighlights extends Disposable {
360369
const id = options?.domId && `${options?.domId}_${i}`;
361370

362371
const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });
363-
const highlightedLabel = this._register(new HighlightedLabel(dom.append(this.container, name), { supportIcons: this.supportIcons }));
364-
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines);
372+
const highlightedLabel = this._register(new HighlightedLabel(dom.append(this.container, name)));
373+
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines, supportIcons);
365374

366375
if (i < label.length - 1) {
367376
dom.append(name, dom.$('span.label-separator', undefined, separator));

src/vs/base/browser/ui/iconLabel/iconlabel.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@
7878
opacity: .95;
7979
}
8080

81+
.monaco-icon-label.bold > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
82+
.monaco-icon-label.bold > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
83+
font-weight: bold;
84+
}
85+
8186
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
8287
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
8388
font-style: italic;

src/vs/base/test/browser/highlightedLabel.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ suite('HighlightedLabel', () => {
1111
let label: HighlightedLabel;
1212

1313
setup(() => {
14-
label = new HighlightedLabel(document.createElement('div'), { supportIcons: true });
14+
label = new HighlightedLabel(document.createElement('div'));
1515
});
1616

1717
test('empty label', function () {

src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,10 @@ export class InlineCompletionsSource extends Disposable {
268268
const result = suggestions.map(c => ({
269269
range: c.editRange.toString(),
270270
text: c.insertText,
271-
isInlineEdit: !!c.isInlineEdit,
272-
source: c.source.provider.groupId,
273271
displayLocation: c.displayLocation ? { label: c.displayLocation.label, range: c.displayLocation.range.toString(), kind: c.displayLocation.kind, jumpToEdit: c.displayLocation.jumpToEdit } : undefined,
272+
isInlineEdit: c.isInlineEdit,
273+
showInlineEditMenu: c.showInlineEditMenu,
274+
providerId: c.source.provider.providerId?.toString(),
274275
}));
275276
this._log({ sourceId: 'InlineCompletions.fetch', kind: 'end', requestId, durationMs: (Date.now() - startTime.getTime()), error, result, time: Date.now(), didAllProvidersReturn });
276277
}

src/vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from '../../../../base/common/async.js';
7-
import { Disposable } from '../../../../base/common/lifecycle.js';
7+
import { Disposable, dispose, IDisposable } from '../../../../base/common/lifecycle.js';
88
import { ICodeEditor } from '../../../browser/editorBrowser.js';
99
import { EditorContributionInstantiation, registerEditorContribution } from '../../../browser/editorExtensions.js';
1010
import { Range } from '../../../common/core/range.js';
@@ -35,6 +35,7 @@ export class ViewportSemanticTokensContribution extends Disposable implements IE
3535
private readonly _debounceInformation: IFeatureDebounceInformation;
3636
private readonly _tokenizeViewport: RunOnceScheduler;
3737
private _outstandingRequests: CancelablePromise<any>[];
38+
private _rangeProvidersChangeListeners: IDisposable[];
3839

3940
constructor(
4041
editor: ICodeEditor,
@@ -50,23 +51,44 @@ export class ViewportSemanticTokensContribution extends Disposable implements IE
5051
this._debounceInformation = languageFeatureDebounceService.for(this._provider, 'DocumentRangeSemanticTokens', { min: 100, max: 500 });
5152
this._tokenizeViewport = this._register(new RunOnceScheduler(() => this._tokenizeViewportNow(), 100));
5253
this._outstandingRequests = [];
54+
this._rangeProvidersChangeListeners = [];
5355
const scheduleTokenizeViewport = () => {
5456
if (this._editor.hasModel()) {
5557
this._tokenizeViewport.schedule(this._debounceInformation.get(this._editor.getModel()));
5658
}
5759
};
60+
const bindRangeProvidersChangeListeners = () => {
61+
this._cleanupProviderListeners();
62+
if (this._editor.hasModel()) {
63+
const model = this._editor.getModel();
64+
for (const provider of this._provider.all(model)) {
65+
const disposable = provider.onDidChange?.(() => {
66+
this._cancelAll();
67+
scheduleTokenizeViewport();
68+
});
69+
if (disposable) {
70+
this._rangeProvidersChangeListeners.push(disposable);
71+
}
72+
}
73+
}
74+
};
75+
5876
this._register(this._editor.onDidScrollChange(() => {
5977
scheduleTokenizeViewport();
6078
}));
6179
this._register(this._editor.onDidChangeModel(() => {
80+
bindRangeProvidersChangeListeners();
6281
this._cancelAll();
6382
scheduleTokenizeViewport();
6483
}));
6584
this._register(this._editor.onDidChangeModelContent((e) => {
6685
this._cancelAll();
6786
scheduleTokenizeViewport();
6887
}));
88+
89+
bindRangeProvidersChangeListeners();
6990
this._register(this._provider.onDidChange(() => {
91+
bindRangeProvidersChangeListeners();
7092
this._cancelAll();
7193
scheduleTokenizeViewport();
7294
}));
@@ -83,6 +105,16 @@ export class ViewportSemanticTokensContribution extends Disposable implements IE
83105
scheduleTokenizeViewport();
84106
}
85107

108+
public override dispose(): void {
109+
this._cleanupProviderListeners();
110+
super.dispose();
111+
}
112+
113+
private _cleanupProviderListeners(): void {
114+
dispose(this._rangeProvidersChangeListeners);
115+
this._rangeProvidersChangeListeners = [];
116+
}
117+
86118
private _cancelAll(): void {
87119
for (const request of this._outstandingRequests) {
88120
request.cancel();
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import assert from 'assert';
7+
import { Barrier, timeout } from '../../../../../base/common/async.js';
8+
import { CancellationToken } from '../../../../../base/common/cancellation.js';
9+
import { Emitter } from '../../../../../base/common/event.js';
10+
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
11+
import { mock } from '../../../../../base/test/common/mock.js';
12+
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
13+
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
14+
import { Range } from '../../../../common/core/range.js';
15+
import { DocumentRangeSemanticTokensProvider, SemanticTokens, SemanticTokensLegend } from '../../../../common/languages.js';
16+
import { ILanguageService } from '../../../../common/languages/language.js';
17+
import { ITextModel } from '../../../../common/model.js';
18+
import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from '../../../../common/services/languageFeatureDebounce.js';
19+
import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';
20+
import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js';
21+
import { LanguageService } from '../../../../common/services/languageService.js';
22+
import { ISemanticTokensStylingService } from '../../../../common/services/semanticTokensStyling.js';
23+
import { SemanticTokensStylingService } from '../../../../common/services/semanticTokensStylingService.js';
24+
import { ViewportSemanticTokensContribution } from '../../browser/viewportSemanticTokens.js';
25+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
26+
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
27+
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
28+
import { NullLogService } from '../../../../../platform/log/common/log.js';
29+
import { ColorScheme } from '../../../../../platform/theme/common/theme.js';
30+
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
31+
import { TestColorTheme, TestThemeService } from '../../../../../platform/theme/test/common/testThemeService.js';
32+
import { createTextModel } from '../../../../test/common/testTextModel.js';
33+
import { createTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
34+
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
35+
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
36+
37+
suite('ViewportSemanticTokens', () => {
38+
39+
const disposables = new DisposableStore();
40+
let languageService: ILanguageService;
41+
let languageFeaturesService: ILanguageFeaturesService;
42+
let serviceCollection: ServiceCollection;
43+
44+
setup(() => {
45+
const configService = new TestConfigurationService({ editor: { semanticHighlighting: true } });
46+
const themeService = new TestThemeService();
47+
themeService.setTheme(new TestColorTheme({}, ColorScheme.DARK, true));
48+
languageFeaturesService = new LanguageFeaturesService();
49+
languageService = disposables.add(new LanguageService(false));
50+
51+
const logService = new NullLogService();
52+
const semanticTokensStylingService = new SemanticTokensStylingService(themeService, logService, languageService);
53+
const envService = new class extends mock<IEnvironmentService>() {
54+
override isBuilt: boolean = true;
55+
override isExtensionDevelopment: boolean = false;
56+
};
57+
const languageFeatureDebounceService = new LanguageFeatureDebounceService(logService, envService);
58+
59+
serviceCollection = new ServiceCollection(
60+
[ILanguageFeaturesService, languageFeaturesService],
61+
[ILanguageFeatureDebounceService, languageFeatureDebounceService],
62+
[ISemanticTokensStylingService, semanticTokensStylingService],
63+
[IThemeService, themeService],
64+
[IConfigurationService, configService]
65+
);
66+
});
67+
68+
teardown(() => {
69+
disposables.clear();
70+
});
71+
72+
ensureNoDisposablesAreLeakedInTestSuite();
73+
74+
test('DocumentRangeSemanticTokens provider onDidChange event should trigger refresh', async () => {
75+
await runWithFakedTimers({}, async () => {
76+
77+
disposables.add(languageService.registerLanguage({ id: 'testMode' }));
78+
79+
const inFirstCall = new Barrier();
80+
const inRefreshCall = new Barrier();
81+
82+
const emitter = new Emitter<void>();
83+
let requestCount = 0;
84+
disposables.add(languageFeaturesService.documentRangeSemanticTokensProvider.register('testMode', new class implements DocumentRangeSemanticTokensProvider {
85+
onDidChange = emitter.event;
86+
getLegend(): SemanticTokensLegend {
87+
return { tokenTypes: ['class'], tokenModifiers: [] };
88+
}
89+
async provideDocumentRangeSemanticTokens(model: ITextModel, range: Range, token: CancellationToken): Promise<SemanticTokens | null> {
90+
requestCount++;
91+
if (requestCount === 1) {
92+
inFirstCall.open();
93+
} else if (requestCount === 2) {
94+
inRefreshCall.open();
95+
}
96+
return {
97+
data: new Uint32Array([0, 1, 1, 1, 1])
98+
};
99+
}
100+
}));
101+
102+
const textModel = disposables.add(createTextModel('Hello world', 'testMode'));
103+
const editor = disposables.add(createTestCodeEditor(textModel, { serviceCollection }));
104+
const instantiationService = new TestInstantiationService(serviceCollection);
105+
disposables.add(instantiationService.createInstance(ViewportSemanticTokensContribution, editor));
106+
107+
textModel.onBeforeAttached();
108+
109+
await inFirstCall.wait();
110+
111+
assert.strictEqual(requestCount, 1, 'Initial request should have been made');
112+
113+
// Make sure no other requests are made for a little while
114+
await timeout(1000);
115+
assert.strictEqual(requestCount, 1, 'No additional requests should have been made');
116+
117+
// Fire the provider's onDidChange event
118+
emitter.fire();
119+
120+
await inRefreshCall.wait();
121+
122+
assert.strictEqual(requestCount, 2, 'Provider onDidChange should trigger a refresh of viewport semantic tokens');
123+
});
124+
});
125+
});

0 commit comments

Comments
 (0)