Skip to content
71 changes: 45 additions & 26 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export interface IEditorOptions {
* Enable quick suggestions (shadow suggestions)
* Defaults to true.
*/
quickSuggestions?: boolean | IQuickSuggestionsOptions;
quickSuggestions?: boolean | QuickSuggestionsValue | IQuickSuggestionsOptions;
/**
* Quick suggestions show delay (in ms)
* Defaults to 10 (ms)
Expand Down Expand Up @@ -3712,7 +3712,7 @@ class PlaceholderOption extends BaseEditorOption<EditorOption.placeholder, strin

//#region quickSuggestions

export type QuickSuggestionsValue = 'on' | 'inline' | 'off';
export type QuickSuggestionsValue = 'on' | 'inline' | 'off' | 'offWhenInlineCompletions';

/**
* Configuration options for quick suggestions
Expand All @@ -3729,7 +3729,7 @@ export interface InternalQuickSuggestionsOptions {
readonly strings: QuickSuggestionsValue;
}

class EditorQuickSuggestions extends BaseEditorOption<EditorOption.quickSuggestions, boolean | IQuickSuggestionsOptions, InternalQuickSuggestionsOptions> {
class EditorQuickSuggestions extends BaseEditorOption<EditorOption.quickSuggestions, boolean | QuickSuggestionsValue | IQuickSuggestionsOptions, InternalQuickSuggestionsOptions> {

public override readonly defaultValue: InternalQuickSuggestionsOptions;

Expand All @@ -3743,32 +3743,45 @@ class EditorQuickSuggestions extends BaseEditorOption<EditorOption.quickSuggesti
{ type: 'boolean' },
{
type: 'string',
enum: ['on', 'inline', 'off'],
enumDescriptions: [nls.localize('on', "Quick suggestions show inside the suggest widget"), nls.localize('inline', "Quick suggestions show as ghost text"), nls.localize('off', "Quick suggestions are disabled")]
enum: ['on', 'inline', 'off', 'offWhenInlineCompletions'],
enumDescriptions: [nls.localize('on', "Quick suggestions show inside the suggest widget"), nls.localize('inline', "Quick suggestions show as ghost text"), nls.localize('off', "Quick suggestions are disabled"), nls.localize('offWhenInlineCompletions', "Quick suggestions are disabled when an inline completion provider is available")]
}
];
super(EditorOption.quickSuggestions, 'quickSuggestions', defaults, {
type: 'object',
additionalProperties: false,
properties: {
strings: {
anyOf: types,
default: defaults.strings,
description: nls.localize('quickSuggestions.strings', "Enable quick suggestions inside strings.")
},
comments: {
anyOf: types,
default: defaults.comments,
description: nls.localize('quickSuggestions.comments', "Enable quick suggestions inside comments.")
},
other: {
anyOf: types,
default: defaults.other,
description: nls.localize('quickSuggestions.other', "Enable quick suggestions outside of strings and comments.")
anyOf: [
{ type: 'boolean' },
{
type: 'string',
enum: ['on', 'inline', 'off', 'offWhenInlineCompletions'],
enumDescriptions: [nls.localize('quickSuggestions.topLevel.on', "Quick suggestions are enabled for all token types"), nls.localize('quickSuggestions.topLevel.inline', "Quick suggestions show as ghost text for all token types"), nls.localize('quickSuggestions.topLevel.off', "Quick suggestions are disabled for all token types"), nls.localize('quickSuggestions.topLevel.offWhenInlineCompletions', "Quick suggestions are disabled for all token types when an inline completion provider is available")]
},
},
{
type: 'object',
additionalProperties: false,
properties: {
strings: {
anyOf: types,
default: defaults.strings,
description: nls.localize('quickSuggestions.strings', "Enable quick suggestions inside strings.")
},
comments: {
anyOf: types,
default: defaults.comments,
description: nls.localize('quickSuggestions.comments', "Enable quick suggestions inside comments.")
},
other: {
anyOf: types,
default: defaults.other,
description: nls.localize('quickSuggestions.other', "Enable quick suggestions outside of strings and comments.")
},
},
}
],
default: defaults,
markdownDescription: nls.localize('quickSuggestions', "Controls whether suggestions should automatically show up while typing. This can be controlled for typing in comments, strings, and other code. Quick suggestion can be configured to show as ghost text or with the suggest widget. Also be aware of the {0}-setting which controls if suggestions are triggered by special characters.", '`#editor.suggestOnTriggerCharacters#`')
markdownDescription: nls.localize('quickSuggestions', "Controls whether suggestions should automatically show up while typing. This can be controlled for typing in comments, strings, and other code. Quick suggestion can be configured to show as ghost text or with the suggest widget. Also be aware of the {0}-setting which controls if suggestions are triggered by special characters.", '`#editor.suggestOnTriggerCharacters#`'),
experiment: {
mode: 'auto'
}
});
this.defaultValue = defaults;
}
Expand All @@ -3779,13 +3792,19 @@ class EditorQuickSuggestions extends BaseEditorOption<EditorOption.quickSuggesti
const value = input ? 'on' : 'off';
return { comments: value, strings: value, other: value };
}
if (typeof input === 'string') {
// string shorthand -> apply same value to all token types
const allowedValues: QuickSuggestionsValue[] = ['on', 'inline', 'off', 'offWhenInlineCompletions'];
const validated = stringSet<QuickSuggestionsValue>(input as QuickSuggestionsValue, this.defaultValue.other, allowedValues);
return { comments: validated, strings: validated, other: validated };
}
if (!input || typeof input !== 'object') {
// invalid object
// invalid input
return this.defaultValue;
}

const { other, comments, strings } = (<IQuickSuggestionsOptions>input);
const allowedValues: QuickSuggestionsValue[] = ['on', 'inline', 'off'];
const allowedValues: QuickSuggestionsValue[] = ['on', 'inline', 'off', 'offWhenInlineCompletions'];
let validatedOther: QuickSuggestionsValue;
let validatedComments: QuickSuggestionsValue;
let validatedStrings: QuickSuggestionsValue;
Expand Down
5 changes: 4 additions & 1 deletion src/vs/editor/contrib/suggest/browser/suggestModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,10 @@ export class SuggestModel implements IDisposable {
const lineTokens = model.tokenization.getLineTokens(pos.lineNumber);
const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0)));
if (QuickSuggestionsOptions.valueFor(config, tokenType) !== 'on') {
return;
if (QuickSuggestionsOptions.valueFor(config, tokenType) !== 'offWhenInlineCompletions'
|| (this._languageFeaturesService.inlineCompletionsProvider.has(model) && this._editor.getOption(EditorOption.inlineSuggest).enabled)) {
return;
}
}
}

Expand Down
141 changes: 140 additions & 1 deletion src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Selection } from '../../../../common/core/selection.js';
import { Handler } from '../../../../common/editorCommon.js';
import { ITextModel } from '../../../../common/model.js';
import { TextModel } from '../../../../common/model/textModel.js';
import { CompletionItemKind, CompletionItemProvider, CompletionList, CompletionTriggerKind, EncodedTokenizationResult, IState, TokenizationRegistry } from '../../../../common/languages.js';
import { CompletionItemKind, CompletionItemProvider, CompletionList, CompletionTriggerKind, EncodedTokenizationResult, InlineCompletionsProvider, IState, TokenizationRegistry } from '../../../../common/languages.js';
import { MetadataConsts } from '../../../../common/encodedTokenAttributes.js';
import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';
import { NullState } from '../../../../common/languages/nullTokenize.js';
Expand All @@ -41,6 +41,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com
import { getSnippetSuggestSupport, setSnippetSuggestSupport } from '../../browser/suggest.js';
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';


function createMockEditor(model: TextModel, languageFeaturesService: ILanguageFeaturesService): ITestCodeEditor {
Expand Down Expand Up @@ -1228,4 +1229,142 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {

});
});

test('offWhenInlineCompletions - suppresses quick suggest when inline provider exists', function () {

disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));

// Register a dummy inline completions provider
const inlineProvider: InlineCompletionsProvider = {
provideInlineCompletions: () => ({ items: [] }),
disposeInlineCompletions: () => { }
};
disposables.add(languageFeaturesService.inlineCompletionsProvider.register({ scheme: 'test' }, inlineProvider));

return withOracle((suggestOracle, editor) => {
editor.updateOptions({ quickSuggestions: { comments: 'off', strings: 'off', other: 'offWhenInlineCompletions' } });

return new Promise<void>((resolve, reject) => {
const unexpectedSuggestSub = suggestOracle.onDidSuggest(() => {
unexpectedSuggestSub.dispose();
reject(new Error('Quick suggestions should not have been triggered'));
});

editor.setPosition({ lineNumber: 1, column: 4 });
editor.trigger('keyboard', Handler.Type, { text: 'd' });

// Wait for the quick suggest delay to pass without triggering
setTimeout(() => {
unexpectedSuggestSub.dispose();
resolve();
}, 200);
});
});
});

test('offWhenInlineCompletions - allows quick suggest when no inline provider exists', function () {

disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));

// No inline completions provider registered for 'test' scheme

return withOracle((suggestOracle, editor) => {
editor.updateOptions({ quickSuggestions: { comments: 'off', strings: 'off', other: 'offWhenInlineCompletions' } });

return assertEvent(suggestOracle.onDidSuggest, () => {
editor.setPosition({ lineNumber: 1, column: 4 });
editor.trigger('keyboard', Handler.Type, { text: 'd' });
}, suggestEvent => {
assert.strictEqual(suggestEvent.triggerOptions.auto, true);
assert.strictEqual(suggestEvent.completionModel.items.length, 1);
});
});
});

test('offWhenInlineCompletions - allows quick suggest when inlineSuggest is disabled', function () {
return runWithFakedTimers({ useFakeTimers: true }, () => {
disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));

// Register a dummy inline completions provider
const inlineProvider: InlineCompletionsProvider = {
provideInlineCompletions: () => ({ items: [] }),
disposeInlineCompletions: () => { }
};
disposables.add(languageFeaturesService.inlineCompletionsProvider.register({ scheme: 'test' }, inlineProvider));

return withOracle((suggestOracle, editor) => {
editor.updateOptions({
quickSuggestions: { comments: 'off', strings: 'off', other: 'offWhenInlineCompletions' },
inlineSuggest: { enabled: false }
});

return assertEvent(suggestOracle.onDidSuggest, () => {
editor.setPosition({ lineNumber: 1, column: 4 });
editor.trigger('keyboard', Handler.Type, { text: 'd' });
}, suggestEvent => {
assert.strictEqual(suggestEvent.triggerOptions.auto, true);
assert.strictEqual(suggestEvent.completionModel.items.length, 1);
});
});
});
});

test('string shorthand - "off" disables quick suggestions for all token types', function () {
return runWithFakedTimers({ useFakeTimers: true }, () => {

disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));

return withOracle((suggestOracle, editor) => {
// Use string shorthand instead of object form
editor.updateOptions({ quickSuggestions: 'off' });

return new Promise<void>((resolve, reject) => {
const sub = suggestOracle.onDidSuggest(() => {
sub.dispose();
reject(new Error('Quick suggestions should have been suppressed by string shorthand "off"'));
});

editor.setPosition({ lineNumber: 1, column: 4 });
editor.trigger('keyboard', Handler.Type, { text: 'd' });

setTimeout(() => {
sub.dispose();
resolve();
}, 200);
});
});
});
});

test('string shorthand - "offWhenInlineCompletions" suppresses when inline provider exists', function () {
return runWithFakedTimers({ useFakeTimers: true }, () => {
disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));

const inlineProvider: InlineCompletionsProvider = {
provideInlineCompletions: () => ({ items: [] }),
disposeInlineCompletions: () => { }
};
disposables.add(languageFeaturesService.inlineCompletionsProvider.register({ scheme: 'test' }, inlineProvider));

return withOracle((suggestOracle, editor) => {
// Use string shorthand — applies to all token types
editor.updateOptions({ quickSuggestions: 'offWhenInlineCompletions' });

return new Promise<void>((resolve, reject) => {
const sub = suggestOracle.onDidSuggest(() => {
sub.dispose();
reject(new Error('Quick suggestions should have been suppressed by offWhenInlineCompletions shorthand'));
});

editor.setPosition({ lineNumber: 1, column: 4 });
editor.trigger('keyboard', Handler.Type, { text: 'd' });

setTimeout(() => {
sub.dispose();
resolve();
}, 200);
});
});
});
});
});
6 changes: 3 additions & 3 deletions src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3645,7 +3645,7 @@ declare namespace monaco.editor {
* Enable quick suggestions (shadow suggestions)
* Defaults to true.
*/
quickSuggestions?: boolean | IQuickSuggestionsOptions;
quickSuggestions?: boolean | QuickSuggestionsValue | IQuickSuggestionsOptions;
/**
* Quick suggestions show delay (in ms)
* Defaults to 10 (ms)
Expand Down Expand Up @@ -4587,7 +4587,7 @@ declare namespace monaco.editor {
cycle?: boolean;
}

export type QuickSuggestionsValue = 'on' | 'inline' | 'off';
export type QuickSuggestionsValue = 'on' | 'inline' | 'off' | 'offWhenInlineCompletions';

/**
* Configuration options for quick suggestions
Expand Down Expand Up @@ -5374,7 +5374,7 @@ declare namespace monaco.editor {
showUnused: IEditorOption<EditorOption.showUnused, boolean>;
showDeprecated: IEditorOption<EditorOption.showDeprecated, boolean>;
inlayHints: IEditorOption<EditorOption.inlayHints, Readonly<Required<IEditorInlayHintsOptions>>>;
snippetSuggestions: IEditorOption<EditorOption.snippetSuggestions, 'none' | 'top' | 'bottom' | 'inline'>;
snippetSuggestions: IEditorOption<EditorOption.snippetSuggestions, 'none' | 'inline' | 'top' | 'bottom'>;
smartSelect: IEditorOption<EditorOption.smartSelect, Readonly<Required<ISmartSelectOptions>>>;
smoothScrolling: IEditorOption<EditorOption.smoothScrolling, boolean>;
stopRenderingLineAfter: IEditorOption<EditorOption.stopRenderingLineAfter, number>;
Expand Down
Loading