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

joh/innocent tiger #155321

Merged
merged 6 commits into from
Jul 15, 2022
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
2 changes: 1 addition & 1 deletion src/vs/base/browser/formattedTextRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { DisposableStore } from 'vs/base/common/lifecycle';

export interface IContentActionHandler {
callback: (content: string, event?: IMouseEvent) => void;
callback: (content: string, event: IMouseEvent) => void;
readonly disposables: DisposableStore;
}

Expand Down
106 changes: 49 additions & 57 deletions src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as dom from 'vs/base/browser/dom';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { localize } from 'vs/nls';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
Expand All @@ -17,9 +17,10 @@ import { Schemas } from 'vs/base/common/network';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions';
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { EventType as GestureEventType, Gesture } from 'vs/base/browser/touch';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IContentActionHandler, renderFormattedText } from 'vs/base/browser/formattedTextRenderer';
import { SelectSnippetForEmptyFile } from 'vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets';

const $ = dom.$;

Expand Down Expand Up @@ -70,7 +71,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
private static readonly ID = 'editor.widget.untitledHint';

private domNode: HTMLElement | undefined;
private toDispose: IDisposable[];
private toDispose: DisposableStore;

constructor(
private readonly editor: ICodeEditor,
Expand All @@ -79,9 +80,9 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
private readonly configurationService: IConfigurationService,
private readonly keybindingService: IKeybindingService,
) {
this.toDispose = [];
this.toDispose.push(editor.onDidChangeModelContent(() => this.onDidChangeModelContent()));
this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
this.toDispose = new DisposableStore();
this.toDispose.add(editor.onDidChangeModelContent(() => this.onDidChangeModelContent()));
this.toDispose.add(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
if (this.domNode && e.hasChanged(EditorOption.fontInfo)) {
this.editor.applyFontInfo(this.domNode);
}
Expand All @@ -107,59 +108,56 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
this.domNode = $('.untitled-hint');
this.domNode.style.width = 'max-content';

const language = $('a.language-mode');
language.style.cursor = 'pointer';
language.innerText = localize('selectAlanguage2', "Select a language");
const languageKeyBinding = this.keybindingService.lookupKeybinding(ChangeLanguageAction.ID);
const languageKeybindingLabel = languageKeyBinding?.getLabel();
if (languageKeybindingLabel) {
language.title = localize('keyboardBindingTooltip', "{0}", languageKeybindingLabel);
}
this.domNode.appendChild(language);

const or = $('span');
or.innerText = localize('or', " or ",);
this.domNode.appendChild(or);

const editorType = $('a.editor-type');
editorType.style.cursor = 'pointer';
editorType.innerText = localize('openADifferentEditor', "open a different editor");
const selectEditorTypeKeyBinding = this.keybindingService.lookupKeybinding('welcome.showNewFileEntries');
const selectEditorTypeKeybindingLabel = selectEditorTypeKeyBinding?.getLabel();
if (selectEditorTypeKeybindingLabel) {
editorType.title = localize('keyboardBindingTooltip', "{0}", selectEditorTypeKeybindingLabel);
}
this.domNode.appendChild(editorType);

const toGetStarted = $('span');
toGetStarted.innerText = localize('toGetStarted', " to get started.");
this.domNode.appendChild(toGetStarted);

this.domNode.appendChild($('br'));

const startTyping = $('span');
startTyping.innerText = localize('startTyping', "Start typing to dismiss or ");
this.domNode.appendChild(startTyping);
const hintMsg = localize({ key: 'message', comment: ['Presereve double-square brackets and their order'] }, '[[Select a language]], [[start with a snippet]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don\'t show]] this again.');
const hintHandler: IContentActionHandler = {
disposables: this.toDispose,
callback: (index, event) => {
switch (index) {
case '0':
languageOnClickOrTap(event.browserEvent);
break;
case '1':
snippetOnClickOrTab(event.browserEvent);
break;
case '2':
chooseEditorOnClickOrTap(event.browserEvent);
break;
case '3':
dontShowOnClickOrTap();
break;
}
}
};

const dontShow = $('a');
dontShow.style.cursor = 'pointer';
dontShow.innerText = localize('dontshow', "don't show");
this.domNode.appendChild(dontShow);
const hintElement = renderFormattedText(hintMsg, {
actionHandler: hintHandler,
renderCodeSegments: false,
});
this.domNode.append(hintElement);

// ugly way to associate keybindings...
const keybindingsLookup = [ChangeLanguageAction.ID, SelectSnippetForEmptyFile.Id, 'welcome.showNewFileEntries'];
for (const anchor of hintElement.querySelectorAll('A')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getElementsByTagName is slightly faster

(<HTMLAnchorElement>anchor).style.cursor = 'pointer';
const id = keybindingsLookup.shift();
const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel();
(<HTMLAnchorElement>anchor).title = title ?? '';
}

const thisAgain = $('span');
thisAgain.innerText = localize('thisAgain', " this again.");
this.domNode.appendChild(thisAgain);
this.toDispose.push(Gesture.addTarget(this.domNode));
// the actual command handlers...
const languageOnClickOrTap = async (e: MouseEvent) => {
e.stopPropagation();
// Need to focus editor before so current editor becomes active and the command is properly executed
this.editor.focus();
await this.commandService.executeCommand(ChangeLanguageAction.ID, { from: 'hint' });
this.editor.focus();
};
this.toDispose.push(dom.addDisposableListener(language, 'click', languageOnClickOrTap));
this.toDispose.push(dom.addDisposableListener(language, GestureEventType.Tap, languageOnClickOrTap));
this.toDispose.push(Gesture.addTarget(language));

const snippetOnClickOrTab = async (e: MouseEvent) => {
e.stopPropagation();
this.editor.focus();
this.commandService.executeCommand(SelectSnippetForEmptyFile.Id, { from: 'hint' });
};

const chooseEditorOnClickOrTap = async (e: MouseEvent) => {
e.stopPropagation();
Expand All @@ -172,20 +170,14 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
this.editorGroupsService.activeGroup.closeEditor(activeEditorInput, { preserveFocus: true });
}
};
this.toDispose.push(dom.addDisposableListener(editorType, 'click', chooseEditorOnClickOrTap));
this.toDispose.push(dom.addDisposableListener(editorType, GestureEventType.Tap, chooseEditorOnClickOrTap));
this.toDispose.push(Gesture.addTarget(editorType));

const dontShowOnClickOrTap = () => {
this.configurationService.updateValue(untitledTextEditorHintSetting, 'hidden');
this.dispose();
this.editor.focus();
};
this.toDispose.push(dom.addDisposableListener(dontShow, 'click', dontShowOnClickOrTap));
this.toDispose.push(dom.addDisposableListener(dontShow, GestureEventType.Tap, dontShowOnClickOrTap));
this.toDispose.push(Gesture.addTarget(dontShow));

this.toDispose.push(dom.addDisposableListener(this.domNode, 'click', () => {
this.toDispose.add(dom.addDisposableListener(this.domNode, 'click', () => {
this.editor.focus();
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { EditorAction2 } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { Action2, IAction2Options } from 'vs/platform/actions/common/actions';

const defaultOptions: Partial<IAction2Options> = {
category: {
value: localize('snippets', 'Snippets'),
original: 'Snippets'
},
};

export abstract class SnippetsAction extends Action2 {

constructor(desc: Readonly<IAction2Options>) {
super({ ...defaultOptions, ...desc });
}
}

export abstract class SnippetEditorAction extends EditorAction2 {

constructor(desc: Readonly<IAction2Options>) {
super({ ...defaultOptions, ...desc });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { isValidBasename } from 'vs/base/common/extpath';
import { extname } from 'vs/base/common/path';
import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { basename, joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution';
import { IQuickPickItem, IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ILanguageService } from 'vs/editor/common/languages/language';
import * as nls from 'vs/nls';
import { MenuId } from 'vs/platform/actions/common/actions';
import { IFileService } from 'vs/platform/files/common/files';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { isValidBasename } from 'vs/base/common/extpath';
import { joinPath, basename } from 'vs/base/common/resources';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { SnippetsAction } from 'vs/workbench/contrib/snippets/browser/commands/abstractSnippetsActions';
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets';
import { SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';

namespace ISnippetPick {
Expand Down Expand Up @@ -199,7 +200,7 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS
await textFileService.write(pick.filepath, contents);
}

registerAction2(class ConfigureSnippets extends Action2 {
export class ConfigureSnippets extends SnippetsAction {

constructor() {
super({
Expand All @@ -221,7 +222,7 @@ registerAction2(class ConfigureSnippets extends Action2 {
});
}

async run(accessor: ServicesAccessor, ...args: any[]): Promise<any> {
async run(accessor: ServicesAccessor): Promise<any> {

const snippetService = accessor.get(ISnippetsService);
const quickInputService = accessor.get(IQuickInputService);
Expand Down Expand Up @@ -275,4 +276,4 @@ registerAction2(class ConfigureSnippets extends Action2 {
}

}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { groupBy, isFalsyOrEmpty } from 'vs/base/common/arrays';
import { compare } from 'vs/base/common/strings';
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { localize } from 'vs/nls';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { SnippetsAction } from 'vs/workbench/contrib/snippets/browser/commands/abstractSnippetsActions';
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets';
import { Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';

export class SelectSnippetForEmptyFile extends SnippetsAction {

static readonly Id = 'workbench.action.populateFromSnippet';

constructor() {
super({
id: SelectSnippetForEmptyFile.Id,
title: {
value: localize('label', 'Populate from Snippet'),
original: 'Populate from Snippet'
},
f1: true,
});
}

async run(accessor: ServicesAccessor): Promise<void> {
const snippetService = accessor.get(ISnippetsService);
const quickInputService = accessor.get(IQuickInputService);
const editorService = accessor.get(IEditorService);
const langService = accessor.get(ILanguageService);

const editor = getCodeEditor(editorService.activeTextEditorControl);
if (!editor || !editor.hasModel()) {
return;
}

const snippets = await snippetService.getSnippets(undefined, { topLevelSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true });
if (snippets.length === 0) {
return;
}

const selection = await this._pick(quickInputService, langService, snippets);
if (!selection) {
return;
}

if (editor.hasModel()) {
// apply snippet edit -> replaces everything
SnippetController2.get(editor)?.apply([{
range: editor.getModel().getFullModelRange(),
template: selection.snippet.body
}]);

// set language if possible
if (langService.isRegisteredLanguageId(selection.langId)) {
editor.getModel().setMode(selection.langId);
}
}
}

private async _pick(quickInputService: IQuickInputService, langService: ILanguageService, snippets: Snippet[]) {

// spread snippet onto each language it supports
type SnippetAndLanguage = { langId: string; snippet: Snippet };
const all: SnippetAndLanguage[] = [];
for (const snippet of snippets) {
if (isFalsyOrEmpty(snippet.scopes)) {
all.push({ langId: '', snippet });
} else {
for (const langId of snippet.scopes) {
all.push({ langId, snippet });
}
}
}

type SnippetAndLanguagePick = IQuickPickItem & { snippet: SnippetAndLanguage };
const picks: (SnippetAndLanguagePick | IQuickPickSeparator)[] = [];

const groups = groupBy(all, (a, b) => compare(a.langId, b.langId));

for (const group of groups) {
let first = true;
for (const item of group) {

if (first) {
picks.push({
type: 'separator',
label: langService.getLanguageName(item.langId) ?? item.langId
});
first = false;
}

picks.push({
snippet: item,
label: item.snippet.prefix || item.snippet.name,
detail: item.snippet.description
});
}
}

const pick = await quickInputService.pick(picks, {
placeHolder: localize('placeholder', 'Select a snippet'),
matchOnDetail: true,
});

return pick?.snippet;
}
}
Loading