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

Implements experimental inline suggestion hints. #171846

Merged
merged 3 commits into from Jan 20, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/vs/base/browser/dom.ts
Expand Up @@ -1818,8 +1818,23 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
el.id = match.groups['id'];
}

const classNames = [];
if (match.groups['class']) {
el.className = match.groups['class'].replace(/\./g, ' ').trim();
for (const className of match.groups['class'].split('.')) {
if (className !== '') {
classNames.push(className);
}
}
}
if (attributes.className !== undefined) {
for (const className of attributes.className.split('.')) {
if (className !== '') {
classNames.push(className);
}
}
}
if (classNames.length > 0) {
el.className = classNames.join(' ');
}

const result: Record<string, HTMLElement> = {};
Expand All @@ -1842,7 +1857,9 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
}

for (const [key, value] of Object.entries(attributes)) {
if (key === 'style') {
if (key === 'className') {
continue;
} else if (key === 'style') {
for (const [cssKey, cssValue] of Object.entries(value)) {
el.style.setProperty(
camelCaseToHyphenCase(cssKey),
Expand Down
21 changes: 19 additions & 2 deletions src/vs/editor/common/config/editorOptions.ts
Expand Up @@ -3784,6 +3784,9 @@ export interface IInlineSuggestOptions {
* Defaults to `prefix`.
*/
mode?: 'prefix' | 'subword' | 'subwordSmart';

useExperimentalHints?: boolean;
hideHints?: boolean;
}

/**
Expand All @@ -3798,7 +3801,9 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
constructor() {
const defaults: InternalInlineSuggestOptions = {
enabled: true,
mode: 'subwordSmart'
mode: 'subwordSmart',
useExperimentalHints: false,
hideHints: false,
};

super(
Expand All @@ -3808,7 +3813,17 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
type: 'boolean',
default: defaults.enabled,
description: nls.localize('inlineSuggest.enabled', "Controls whether to automatically show inline suggestions in the editor.")
}
},
'editor.inlineSuggest.useExperimentalHints': {
type: 'boolean',
default: defaults.useExperimentalHints,
description: nls.localize('inlineSuggest.useExperimentalHints', "Controls whether to use experimental hints in the editor."),
},
'editor.inlineSuggest.hideHints': {
type: 'boolean',
default: defaults.hideHints,
description: nls.localize('inlineSuggest.hideHints', "Controls whether to hide hints in the editor."),
},
}
);
}
Expand All @@ -3821,6 +3836,8 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
return {
enabled: boolean(input.enabled, this.defaultValue.enabled),
mode: stringSet(input.mode, this.defaultValue.mode, ['prefix', 'subword', 'subwordSmart']),
useExperimentalHints: boolean(input.useExperimentalHints, this.defaultValue.useExperimentalHints),
hideHints: boolean(input.hideHints, this.defaultValue.hideHints),
};
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/vs/editor/contrib/inlineCompletions/browser/consts.ts
Expand Up @@ -4,3 +4,7 @@
*--------------------------------------------------------------------------------------------*/

export const inlineSuggestCommitId = 'editor.action.inlineSuggest.commit';

export const showPreviousInlineSuggestionActionId = 'editor.action.inlineSuggest.showPrevious';

export const showNextInlineSuggestionActionId = 'editor.action.inlineSuggest.showNext';
Expand Up @@ -3,55 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { KeyCode } from 'vs/base/common/keyCodes';
import { EditorCommand, EditorContributionInstantiation, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes';
import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/consts';
import { GhostTextController, AcceptNextWordOfInlineCompletion, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, TriggerInlineSuggestionAction } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController';
import { AcceptInlineCompletion, AcceptNextWordOfInlineCompletion, DisableSuggestionHints, GhostTextController, HideInlineCompletion, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, TriggerInlineSuggestionAction, UndoAcceptPart } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController';
import { InlineCompletionsHoverParticipant } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';

registerEditorContribution(GhostTextController.ID, GhostTextController, EditorContributionInstantiation.Eventually);
registerEditorAction(TriggerInlineSuggestionAction);
registerEditorAction(ShowNextInlineSuggestionAction);
registerEditorAction(ShowPreviousInlineSuggestionAction);
registerEditorAction(AcceptNextWordOfInlineCompletion);
registerEditorAction(AcceptInlineCompletion);
registerEditorAction(HideInlineCompletion);
registerEditorAction(UndoAcceptPart);
registerEditorAction(DisableSuggestionHints);

HoverParticipantRegistry.register(InlineCompletionsHoverParticipant);

const GhostTextCommand = EditorCommand.bindToContribution(GhostTextController.get);

export const commitInlineSuggestionAction = new GhostTextCommand({
id: inlineSuggestCommitId,
precondition: GhostTextController.inlineSuggestionVisible,
handler(x) {
x.commit();
x.editor.focus();
}
});
registerEditorCommand(commitInlineSuggestionAction);

KeybindingsRegistry.registerKeybindingRule({
primary: KeyCode.Tab,
weight: 200,
id: commitInlineSuggestionAction.id,
when: ContextKeyExpr.and(
commitInlineSuggestionAction.precondition,
EditorContextKeys.tabMovesFocus.toNegated(),
GhostTextController.inlineSuggestionHasIndentationLessThanTabSize
),
});

registerEditorCommand(new GhostTextCommand({
id: 'editor.action.inlineSuggest.hide',
precondition: GhostTextController.inlineSuggestionVisible,
kbOpts: {
weight: 100,
primary: KeyCode.Escape,
},
handler(x) {
x.hide();
}
}));
139 changes: 126 additions & 13 deletions src/vs/editor/contrib/inlineCompletions/browser/ghostTextController.ts
Expand Up @@ -14,12 +14,16 @@ import { CursorColumns } from 'vs/editor/common/core/cursorColumns';
import { Range } from 'vs/editor/common/core/range';
import { CursorChangeReason } from 'vs/editor/common/cursorEvents';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { inlineSuggestCommitId, showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from 'vs/editor/contrib/inlineCompletions/browser/consts';
import { GhostTextModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextModel';
import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget';
import { InlineSuggestionHintsWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineSuggestionHintsWidget';
import * as nls from 'vs/nls';
import { MenuId } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';

export class GhostTextController extends Disposable {
public static readonly inlineSuggestionVisible = new RawContextKey<boolean>('inlineSuggestionVisible', false, nls.localize('inlineSuggestionVisible', "Whether an inline suggestion is visible"));
Expand Down Expand Up @@ -140,9 +144,9 @@ export class GhostTextController extends Disposable {
this.activeModel?.showPreviousInlineCompletion();
}

public async hasMultipleInlineCompletions(): Promise<boolean> {
const result = await this.activeModel?.hasMultipleInlineCompletions();
return result !== undefined ? result : false;
public async getInlineCompletionsCount(): Promise<number> {
const result = await this.activeModel?.getInlineCompletionsCount();
return result ?? 0;
}
}

Expand All @@ -165,6 +169,8 @@ export class ActiveGhostTextController extends Disposable {
public readonly model = this._register(this.instantiationService.createInstance(GhostTextModel, this.editor));
public readonly widget = this._register(this.instantiationService.createInstance(GhostTextWidget, this.editor, this.model));

public readonly hintsWidget = this._register(this.instantiationService.createInstance(InlineSuggestionHintsWidget, this.editor, this.model.inlineCompletionsModel));

constructor(
private readonly editor: IActiveCodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService,
Expand Down Expand Up @@ -221,7 +227,7 @@ export class ActiveGhostTextController extends Disposable {


export class ShowNextInlineSuggestionAction extends EditorAction {
public static ID = 'editor.action.inlineSuggest.showNext';
public static ID = showNextInlineSuggestionActionId;
constructor() {
super({
id: ShowNextInlineSuggestionAction.ID,
Expand All @@ -245,7 +251,7 @@ export class ShowNextInlineSuggestionAction extends EditorAction {
}

export class ShowPreviousInlineSuggestionAction extends EditorAction {
public static ID = 'editor.action.inlineSuggest.showPrevious';
public static ID = showPreviousInlineSuggestionActionId;
constructor() {
super({
id: ShowPreviousInlineSuggestionAction.ID,
Expand Down Expand Up @@ -294,7 +300,13 @@ export class AcceptNextWordOfInlineCompletion extends EditorAction {
kbOpts: {
weight: KeybindingWeight.EditorContrib + 1,
primary: KeyMod.CtrlCmd | KeyCode.RightArrow,
}
},
menuOpts: [{
menuId: MenuId.InlineSuggestionToolbar,
title: nls.localize('acceptPart', 'Accept Part'),
group: 'primary',
order: 2,
}],
});
}

Expand All @@ -306,9 +318,110 @@ export class AcceptNextWordOfInlineCompletion extends EditorAction {
}
}

KeybindingsRegistry.registerKeybindingRule({
id: 'undo',
weight: KeybindingWeight.EditorContrib + 1,
primary: KeyMod.CtrlCmd | KeyCode.LeftArrow,
when: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.canUndoInlineSuggestion),
});
export class AcceptInlineCompletion extends EditorAction {
constructor() {
super({
id: inlineSuggestCommitId,
label: nls.localize('action.inlineSuggest.acceptNextWord', "Accept Next Word Of Inline Suggestion"),
alias: 'Accept Next Word Of Inline Suggestion',
precondition: GhostTextController.inlineSuggestionVisible,
menuOpts: [{
menuId: MenuId.InlineSuggestionToolbar,
title: nls.localize('accept', "Accept"),
group: 'primary',
order: 1,
}],
kbOpts: {
primary: KeyCode.Tab,
weight: 200,
kbExpr: ContextKeyExpr.and(
GhostTextController.inlineSuggestionVisible,
EditorContextKeys.tabMovesFocus.toNegated(),
GhostTextController.inlineSuggestionHasIndentationLessThanTabSize
),
}
});
}

public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = GhostTextController.get(editor);
if (controller) {
controller.commit();
controller.editor.focus();
}
}
}

export class HideInlineCompletion extends EditorAction {
public static ID = 'editor.action.inlineSuggest.hide';

constructor() {
super({
id: HideInlineCompletion.ID,
label: nls.localize('action.inlineSuggest.acceptNextWord', "Accept Next Word Of Inline Suggestion"),
alias: 'Accept Next Word Of Inline Suggestion',
precondition: GhostTextController.inlineSuggestionVisible,
kbOpts: {
weight: 100,
primary: KeyCode.Escape,
}
});
}

public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = GhostTextController.get(editor);
if (controller) {
controller.hide();
}
}
}

export class DisableSuggestionHints extends EditorAction {
public static ID = 'editor.action.inlineSuggest.disableHints';

constructor() {
super({
id: DisableSuggestionHints.ID,
label: nls.localize('action.inlineSuggest.disableHints', "Disable suggestion hints"),
alias: 'Disable suggestion hints',
precondition: undefined,
menuOpts: [{
menuId: MenuId.InlineSuggestionToolbar,
title: nls.localize('action.inlineSuggest.disableHints', "Disable suggestion hints"),
group: 'secondary',
order: 10,
}],
});
}

public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
const configService = accessor.get(IConfigurationService);
configService.updateValue('editor.inlineSuggest.hideHints', true);
}
}

export class UndoAcceptPart extends EditorAction {
constructor() {
super({
id: 'editor.action.inlineSuggest.undo',
label: nls.localize('action.inlineSuggest.undo', "Undo Accept Part"),
alias: 'Undo Accept Part',
precondition: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.canUndoInlineSuggestion),
kbOpts: {
weight: KeybindingWeight.EditorContrib + 1,
primary: KeyMod.CtrlCmd | KeyCode.LeftArrow,
kbExpr: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.canUndoInlineSuggestion),
},
menuOpts: [{
menuId: MenuId.InlineSuggestionToolbar,
title: 'Undo Accept Part',
group: 'secondary',
order: 3,
}],
});
}

public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
editor.getModel()?.undo();
}
}
Expand Up @@ -21,6 +21,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/consts';
import { Command } from 'vs/editor/common/languages';
import { EditorOption } from 'vs/editor/common/config/editorOptions';

export class InlineCompletionsHover implements IHoverPart {
constructor(
Expand All @@ -37,8 +38,8 @@ export class InlineCompletionsHover implements IHoverPart {
);
}

public hasMultipleSuggestions(): Promise<boolean> {
return this.controller.hasMultipleInlineCompletions();
public getInlineCompletionsCount(): Promise<number> {
return this.controller.getInlineCompletionsCount();
}

public get commands(): Command[] {
Expand All @@ -58,13 +59,18 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
@ILanguageService private readonly _languageService: ILanguageService,
@IOpenerService private readonly _openerService: IOpenerService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
) { }
) {
}

suggestHoverAnchor(mouseEvent: IEditorMouseEvent): HoverAnchor | null {
const controller = GhostTextController.get(this._editor);
if (!controller) {
return null;
}
if (this._editor.getOption(EditorOption.inlineSuggest).useExperimentalHints) {
return null;
}

const target = mouseEvent.target;
if (target.type === MouseTargetType.CONTENT_VIEW_ZONE) {
// handle the case where the mouse is over the view zone
Expand Down Expand Up @@ -131,9 +137,9 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
for (const action of actions) {
action.setEnabled(false);
}
part.hasMultipleSuggestions().then(hasMore => {
part.getInlineCompletionsCount().then(count => {
for (const action of actions) {
action.setEnabled(hasMore);
action.setEnabled(count > 1);
}
});

Expand Down