Skip to content

Commit 5315bc8

Browse files
authored
Merge pull request microsoft#155321 from microsoft/joh/innocent-tiger
joh/innocent tiger
2 parents 77755b6 + 92329e4 commit 5315bc8

18 files changed

+409
-198
lines changed

src/vs/base/browser/formattedTextRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent';
88
import { DisposableStore } from 'vs/base/common/lifecycle';
99

1010
export interface IContentActionHandler {
11-
callback: (content: string, event?: IMouseEvent) => void;
11+
callback: (content: string, event: IMouseEvent) => void;
1212
readonly disposables: DisposableStore;
1313
}
1414

src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts

Lines changed: 49 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

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

2425
const $ = dom.$;
2526

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

7273
private domNode: HTMLElement | undefined;
73-
private toDispose: IDisposable[];
74+
private toDispose: DisposableStore;
7475

7576
constructor(
7677
private readonly editor: ICodeEditor,
@@ -79,9 +80,9 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
7980
private readonly configurationService: IConfigurationService,
8081
private readonly keybindingService: IKeybindingService,
8182
) {
82-
this.toDispose = [];
83-
this.toDispose.push(editor.onDidChangeModelContent(() => this.onDidChangeModelContent()));
84-
this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
83+
this.toDispose = new DisposableStore();
84+
this.toDispose.add(editor.onDidChangeModelContent(() => this.onDidChangeModelContent()));
85+
this.toDispose.add(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
8586
if (this.domNode && e.hasChanged(EditorOption.fontInfo)) {
8687
this.editor.applyFontInfo(this.domNode);
8788
}
@@ -107,59 +108,56 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
107108
this.domNode = $('.untitled-hint');
108109
this.domNode.style.width = 'max-content';
109110

110-
const language = $('a.language-mode');
111-
language.style.cursor = 'pointer';
112-
language.innerText = localize('selectAlanguage2', "Select a language");
113-
const languageKeyBinding = this.keybindingService.lookupKeybinding(ChangeLanguageAction.ID);
114-
const languageKeybindingLabel = languageKeyBinding?.getLabel();
115-
if (languageKeybindingLabel) {
116-
language.title = localize('keyboardBindingTooltip', "{0}", languageKeybindingLabel);
117-
}
118-
this.domNode.appendChild(language);
119-
120-
const or = $('span');
121-
or.innerText = localize('or', " or ",);
122-
this.domNode.appendChild(or);
123-
124-
const editorType = $('a.editor-type');
125-
editorType.style.cursor = 'pointer';
126-
editorType.innerText = localize('openADifferentEditor', "open a different editor");
127-
const selectEditorTypeKeyBinding = this.keybindingService.lookupKeybinding('welcome.showNewFileEntries');
128-
const selectEditorTypeKeybindingLabel = selectEditorTypeKeyBinding?.getLabel();
129-
if (selectEditorTypeKeybindingLabel) {
130-
editorType.title = localize('keyboardBindingTooltip', "{0}", selectEditorTypeKeybindingLabel);
131-
}
132-
this.domNode.appendChild(editorType);
133-
134-
const toGetStarted = $('span');
135-
toGetStarted.innerText = localize('toGetStarted', " to get started.");
136-
this.domNode.appendChild(toGetStarted);
137-
138-
this.domNode.appendChild($('br'));
139-
140-
const startTyping = $('span');
141-
startTyping.innerText = localize('startTyping', "Start typing to dismiss or ");
142-
this.domNode.appendChild(startTyping);
111+
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.');
112+
const hintHandler: IContentActionHandler = {
113+
disposables: this.toDispose,
114+
callback: (index, event) => {
115+
switch (index) {
116+
case '0':
117+
languageOnClickOrTap(event.browserEvent);
118+
break;
119+
case '1':
120+
snippetOnClickOrTab(event.browserEvent);
121+
break;
122+
case '2':
123+
chooseEditorOnClickOrTap(event.browserEvent);
124+
break;
125+
case '3':
126+
dontShowOnClickOrTap();
127+
break;
128+
}
129+
}
130+
};
143131

144-
const dontShow = $('a');
145-
dontShow.style.cursor = 'pointer';
146-
dontShow.innerText = localize('dontshow', "don't show");
147-
this.domNode.appendChild(dontShow);
132+
const hintElement = renderFormattedText(hintMsg, {
133+
actionHandler: hintHandler,
134+
renderCodeSegments: false,
135+
});
136+
this.domNode.append(hintElement);
137+
138+
// ugly way to associate keybindings...
139+
const keybindingsLookup = [ChangeLanguageAction.ID, SelectSnippetForEmptyFile.Id, 'welcome.showNewFileEntries'];
140+
for (const anchor of hintElement.querySelectorAll('A')) {
141+
(<HTMLAnchorElement>anchor).style.cursor = 'pointer';
142+
const id = keybindingsLookup.shift();
143+
const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel();
144+
(<HTMLAnchorElement>anchor).title = title ?? '';
145+
}
148146

149-
const thisAgain = $('span');
150-
thisAgain.innerText = localize('thisAgain', " this again.");
151-
this.domNode.appendChild(thisAgain);
152-
this.toDispose.push(Gesture.addTarget(this.domNode));
147+
// the actual command handlers...
153148
const languageOnClickOrTap = async (e: MouseEvent) => {
154149
e.stopPropagation();
155150
// Need to focus editor before so current editor becomes active and the command is properly executed
156151
this.editor.focus();
157152
await this.commandService.executeCommand(ChangeLanguageAction.ID, { from: 'hint' });
158153
this.editor.focus();
159154
};
160-
this.toDispose.push(dom.addDisposableListener(language, 'click', languageOnClickOrTap));
161-
this.toDispose.push(dom.addDisposableListener(language, GestureEventType.Tap, languageOnClickOrTap));
162-
this.toDispose.push(Gesture.addTarget(language));
155+
156+
const snippetOnClickOrTab = async (e: MouseEvent) => {
157+
e.stopPropagation();
158+
this.editor.focus();
159+
this.commandService.executeCommand(SelectSnippetForEmptyFile.Id, { from: 'hint' });
160+
};
163161

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

179174
const dontShowOnClickOrTap = () => {
180175
this.configurationService.updateValue(untitledTextEditorHintSetting, 'hidden');
181176
this.dispose();
182177
this.editor.focus();
183178
};
184-
this.toDispose.push(dom.addDisposableListener(dontShow, 'click', dontShowOnClickOrTap));
185-
this.toDispose.push(dom.addDisposableListener(dontShow, GestureEventType.Tap, dontShowOnClickOrTap));
186-
this.toDispose.push(Gesture.addTarget(dontShow));
187179

188-
this.toDispose.push(dom.addDisposableListener(this.domNode, 'click', () => {
180+
this.toDispose.add(dom.addDisposableListener(this.domNode, 'click', () => {
189181
this.editor.focus();
190182
}));
191183

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 { EditorAction2 } from 'vs/editor/browser/editorExtensions';
7+
import { localize } from 'vs/nls';
8+
import { Action2, IAction2Options } from 'vs/platform/actions/common/actions';
9+
10+
const defaultOptions: Partial<IAction2Options> = {
11+
category: {
12+
value: localize('snippets', 'Snippets'),
13+
original: 'Snippets'
14+
},
15+
};
16+
17+
export abstract class SnippetsAction extends Action2 {
18+
19+
constructor(desc: Readonly<IAction2Options>) {
20+
super({ ...defaultOptions, ...desc });
21+
}
22+
}
23+
24+
export abstract class SnippetEditorAction extends EditorAction2 {
25+
26+
constructor(desc: Readonly<IAction2Options>) {
27+
super({ ...defaultOptions, ...desc });
28+
}
29+
}

src/vs/workbench/contrib/snippets/browser/configureSnippets.ts renamed to src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,22 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as nls from 'vs/nls';
7-
import { ILanguageService } from 'vs/editor/common/languages/language';
6+
import { isValidBasename } from 'vs/base/common/extpath';
87
import { extname } from 'vs/base/common/path';
9-
import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
10-
import { IOpenerService } from 'vs/platform/opener/common/opener';
8+
import { basename, joinPath } from 'vs/base/common/resources';
119
import { URI } from 'vs/base/common/uri';
12-
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution';
13-
import { IQuickPickItem, IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
14-
import { SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
15-
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
10+
import { ILanguageService } from 'vs/editor/common/languages/language';
11+
import * as nls from 'vs/nls';
12+
import { MenuId } from 'vs/platform/actions/common/actions';
1613
import { IFileService } from 'vs/platform/files/common/files';
17-
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
18-
import { isValidBasename } from 'vs/base/common/extpath';
19-
import { joinPath, basename } from 'vs/base/common/resources';
2014
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
15+
import { IOpenerService } from 'vs/platform/opener/common/opener';
16+
import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
17+
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
18+
import { SnippetsAction } from 'vs/workbench/contrib/snippets/browser/commands/abstractSnippetsActions';
19+
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets';
20+
import { SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
21+
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
2122
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
2223

2324
namespace ISnippetPick {
@@ -199,7 +200,7 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS
199200
await textFileService.write(pick.filepath, contents);
200201
}
201202

202-
registerAction2(class ConfigureSnippets extends Action2 {
203+
export class ConfigureSnippets extends SnippetsAction {
203204

204205
constructor() {
205206
super({
@@ -221,7 +222,7 @@ registerAction2(class ConfigureSnippets extends Action2 {
221222
});
222223
}
223224

224-
async run(accessor: ServicesAccessor, ...args: any[]): Promise<any> {
225+
async run(accessor: ServicesAccessor): Promise<any> {
225226

226227
const snippetService = accessor.get(ISnippetsService);
227228
const quickInputService = accessor.get(IQuickInputService);
@@ -275,4 +276,4 @@ registerAction2(class ConfigureSnippets extends Action2 {
275276
}
276277

277278
}
278-
});
279+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 { groupBy, isFalsyOrEmpty } from 'vs/base/common/arrays';
7+
import { compare } from 'vs/base/common/strings';
8+
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
9+
import { ILanguageService } from 'vs/editor/common/languages/language';
10+
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
11+
import { localize } from 'vs/nls';
12+
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
13+
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
14+
import { SnippetsAction } from 'vs/workbench/contrib/snippets/browser/commands/abstractSnippetsActions';
15+
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets';
16+
import { Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
17+
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
18+
19+
export class SelectSnippetForEmptyFile extends SnippetsAction {
20+
21+
static readonly Id = 'workbench.action.populateFromSnippet';
22+
23+
constructor() {
24+
super({
25+
id: SelectSnippetForEmptyFile.Id,
26+
title: {
27+
value: localize('label', 'Populate from Snippet'),
28+
original: 'Populate from Snippet'
29+
},
30+
f1: true,
31+
});
32+
}
33+
34+
async run(accessor: ServicesAccessor): Promise<void> {
35+
const snippetService = accessor.get(ISnippetsService);
36+
const quickInputService = accessor.get(IQuickInputService);
37+
const editorService = accessor.get(IEditorService);
38+
const langService = accessor.get(ILanguageService);
39+
40+
const editor = getCodeEditor(editorService.activeTextEditorControl);
41+
if (!editor || !editor.hasModel()) {
42+
return;
43+
}
44+
45+
const snippets = await snippetService.getSnippets(undefined, { topLevelSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true });
46+
if (snippets.length === 0) {
47+
return;
48+
}
49+
50+
const selection = await this._pick(quickInputService, langService, snippets);
51+
if (!selection) {
52+
return;
53+
}
54+
55+
if (editor.hasModel()) {
56+
// apply snippet edit -> replaces everything
57+
SnippetController2.get(editor)?.apply([{
58+
range: editor.getModel().getFullModelRange(),
59+
template: selection.snippet.body
60+
}]);
61+
62+
// set language if possible
63+
if (langService.isRegisteredLanguageId(selection.langId)) {
64+
editor.getModel().setMode(selection.langId);
65+
}
66+
}
67+
}
68+
69+
private async _pick(quickInputService: IQuickInputService, langService: ILanguageService, snippets: Snippet[]) {
70+
71+
// spread snippet onto each language it supports
72+
type SnippetAndLanguage = { langId: string; snippet: Snippet };
73+
const all: SnippetAndLanguage[] = [];
74+
for (const snippet of snippets) {
75+
if (isFalsyOrEmpty(snippet.scopes)) {
76+
all.push({ langId: '', snippet });
77+
} else {
78+
for (const langId of snippet.scopes) {
79+
all.push({ langId, snippet });
80+
}
81+
}
82+
}
83+
84+
type SnippetAndLanguagePick = IQuickPickItem & { snippet: SnippetAndLanguage };
85+
const picks: (SnippetAndLanguagePick | IQuickPickSeparator)[] = [];
86+
87+
const groups = groupBy(all, (a, b) => compare(a.langId, b.langId));
88+
89+
for (const group of groups) {
90+
let first = true;
91+
for (const item of group) {
92+
93+
if (first) {
94+
picks.push({
95+
type: 'separator',
96+
label: langService.getLanguageName(item.langId) ?? item.langId
97+
});
98+
first = false;
99+
}
100+
101+
picks.push({
102+
snippet: item,
103+
label: item.snippet.prefix || item.snippet.name,
104+
detail: item.snippet.description
105+
});
106+
}
107+
}
108+
109+
const pick = await quickInputService.pick(picks, {
110+
placeHolder: localize('placeholder', 'Select a snippet'),
111+
matchOnDetail: true,
112+
});
113+
114+
return pick?.snippet;
115+
}
116+
}

0 commit comments

Comments
 (0)