UNPKG

15.5 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2017 TypeFox and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { inject, injectable, named, postConstruct } from 'inversify';
18import * as fileIcons from 'file-icons-js';
19import URI from '../common/uri';
20import { ContributionProvider } from '../common/contribution-provider';
21import { Event, Emitter, Disposable, isObject, Path, Prioritizeable } from '../common';
22import { FrontendApplicationContribution } from './frontend-application';
23import { EnvVariablesServer } from '../common/env-variables/env-variables-protocol';
24import { ResourceLabelFormatter, ResourceLabelFormatting } from '../common/label-protocol';
25import { codicon } from './widgets';
26
27/**
28 * @internal don't export it, use `LabelProvider.folderIcon` instead.
29 */
30const DEFAULT_FOLDER_ICON = `${codicon('folder')} default-folder-icon`;
31/**
32 * @internal don't export it, use `LabelProvider.fileIcon` instead.
33 */
34const DEFAULT_FILE_ICON = `${codicon('file')} default-file-icon`;
35
36export const LabelProviderContribution = Symbol('LabelProviderContribution');
37/**
38 * A {@link LabelProviderContribution} determines how specific elements/nodes are displayed in the workbench.
39 * Theia views use a common {@link LabelProvider} to determine the label and/or an icon for elements shown in the UI. This includes elements in lists
40 * and trees, but also view specific locations like headers. The common {@link LabelProvider} collects all {@links LabelProviderContribution} and delegates
41 * to the contribution with the highest priority. This is determined via calling the {@link LabelProviderContribution.canHandle} function, so contributions
42 * define which elements they are responsible for.
43 * As arbitrary views can consume LabelProviderContributions, they must be generic for the covered element type, not view specific. Label providers and
44 * contributions can be used for arbitrary element and node types, e.g. for markers or domain-specific elements.
45 */
46export interface LabelProviderContribution {
47
48 /**
49 * Determines whether this contribution can handle the given element and with what priority.
50 * All contributions are ordered by the returned number if greater than zero. The highest number wins.
51 * If two or more contributions return the same positive number one of those will be used. It is undefined which one.
52 */
53 canHandle(element: object): number;
54
55 /**
56 * returns an icon class for the given element.
57 */
58 getIcon?(element: object): string | undefined;
59
60 /**
61 * returns a short name for the given element.
62 */
63 getName?(element: object): string | undefined;
64
65 /**
66 * returns a long name for the given element.
67 */
68 getLongName?(element: object): string | undefined;
69
70 /**
71 * A compromise between {@link getName} and {@link getLongName}. Can be used to supplement getName in contexts that allow both a primary display field and extra detail.
72 */
73 getDetails?(element: object): string | undefined;
74
75 /**
76 * Emit when something has changed that may result in this label provider returning a different
77 * value for one or more properties (name, icon etc).
78 */
79 readonly onDidChange?: Event<DidChangeLabelEvent>;
80
81 /**
82 * Checks whether the given element is affected by the given change event.
83 * Contributions delegating to the label provider can use this hook
84 * to perform a recursive check.
85 */
86 affects?(element: object, event: DidChangeLabelEvent): boolean;
87
88}
89
90export interface DidChangeLabelEvent {
91 affects(element: object): boolean;
92}
93
94export interface URIIconReference {
95 kind: 'uriIconReference';
96 id: 'file' | 'folder';
97 uri?: URI
98}
99export namespace URIIconReference {
100 export function is(element: unknown): element is URIIconReference {
101 return isObject(element) && element.kind === 'uriIconReference';
102 }
103 export function create(id: URIIconReference['id'], uri?: URI): URIIconReference {
104 return { kind: 'uriIconReference', id, uri };
105 }
106}
107
108@injectable()
109export class DefaultUriLabelProviderContribution implements LabelProviderContribution {
110
111 protected formatters: ResourceLabelFormatter[] = [];
112 protected readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
113 protected homePath: string | undefined;
114 @inject(EnvVariablesServer) protected readonly envVariablesServer: EnvVariablesServer;
115
116 @postConstruct()
117 init(): void {
118 this.envVariablesServer.getHomeDirUri().then(result => {
119 this.homePath = result;
120 this.fireOnDidChange();
121 });
122 }
123
124 canHandle(element: object): number {
125 if (element instanceof URI || URIIconReference.is(element)) {
126 return 1;
127 }
128 return 0;
129 }
130
131 getIcon(element: URI | URIIconReference): string {
132 if (URIIconReference.is(element) && element.id === 'folder') {
133 return this.defaultFolderIcon;
134 }
135 const uri = URIIconReference.is(element) ? element.uri : element;
136 if (uri) {
137 const iconClass = uri && this.getFileIcon(uri);
138 return iconClass || this.defaultFileIcon;
139 }
140 return '';
141 }
142
143 get defaultFolderIcon(): string {
144 return DEFAULT_FOLDER_ICON;
145 }
146
147 get defaultFileIcon(): string {
148 return DEFAULT_FILE_ICON;
149 }
150
151 protected getFileIcon(uri: URI): string | undefined {
152 const fileIcon = fileIcons.getClassWithColor(uri.displayName);
153 if (!fileIcon) {
154 return undefined;
155 }
156 return fileIcon + ' theia-file-icons-js';
157 }
158
159 getName(element: URI | URIIconReference): string | undefined {
160 const uri = this.getUri(element);
161 return uri && uri.displayName;
162 }
163
164 getLongName(element: URI | URIIconReference): string | undefined {
165 const uri = this.getUri(element);
166 if (uri) {
167 const formatting = this.findFormatting(uri);
168 if (formatting) {
169 return this.formatUri(uri, formatting);
170 }
171 }
172 return uri && uri.path.fsPath();
173 }
174
175 getDetails(element: URI | URIIconReference): string | undefined {
176 const uri = this.getUri(element);
177 if (uri) {
178 return this.getLongName(uri.parent);
179 }
180 return this.getLongName(element);
181 }
182
183 protected getUri(element: URI | URIIconReference): URI | undefined {
184 return URIIconReference.is(element) ? element.uri : element;
185 }
186
187 registerFormatter(formatter: ResourceLabelFormatter): Disposable {
188 this.formatters.push(formatter);
189 this.fireOnDidChange();
190 return Disposable.create(() => {
191 this.formatters = this.formatters.filter(f => f !== formatter);
192 this.fireOnDidChange();
193 });
194 }
195
196 get onDidChange(): Event<DidChangeLabelEvent> {
197 return this.onDidChangeEmitter.event;
198 }
199
200 private fireOnDidChange(): void {
201 this.onDidChangeEmitter.fire({
202 affects: (element: URI) => this.canHandle(element) > 0
203 });
204 }
205
206 // copied and modified from https://github.com/microsoft/vscode/blob/1.44.2/src/vs/workbench/services/label/common/labelService.ts
207 /*---------------------------------------------------------------------------------------------
208 * Copyright (c) Microsoft Corporation. All rights reserved.
209 * Licensed under the MIT License. See License.txt in the project root for license information.
210 *--------------------------------------------------------------------------------------------*/
211 private readonly labelMatchingRegexp = /\${(scheme|authority|path|query)}/g;
212 protected formatUri(resource: URI, formatting: ResourceLabelFormatting): string {
213 let label = formatting.label.replace(this.labelMatchingRegexp, (match, token) => {
214 switch (token) {
215 case 'scheme': return resource.scheme;
216 case 'authority': return resource.authority;
217 case 'path': return resource.path.toString();
218 case 'query': return resource.query;
219 default: return '';
220 }
221 });
222
223 // convert \c:\something => C:\something
224 if (formatting.normalizeDriveLetter && this.hasDriveLetter(label)) {
225 label = label.charAt(1).toUpperCase() + label.substr(2);
226 }
227
228 if (formatting.tildify) {
229 label = Path.tildify(label, this.homePath ? this.homePath : '');
230 }
231 if (formatting.authorityPrefix && resource.authority) {
232 label = formatting.authorityPrefix + label;
233 }
234
235 return label.replace(/\//g, formatting.separator);
236 }
237
238 private hasDriveLetter(path: string): boolean {
239 return !!(path && path[2] === ':');
240 }
241
242 protected findFormatting(resource: URI): ResourceLabelFormatting | undefined {
243 let bestResult: ResourceLabelFormatter | undefined;
244
245 this.formatters.forEach(formatter => {
246 if (formatter.scheme === resource.scheme) {
247 if (!bestResult && !formatter.authority) {
248 bestResult = formatter;
249 return;
250 }
251 if (!formatter.authority) {
252 return;
253 }
254
255 if ((formatter.authority.toLowerCase() === resource.authority.toLowerCase()) &&
256 (!bestResult || !bestResult.authority || formatter.authority.length > bestResult.authority.length ||
257 ((formatter.authority.length === bestResult.authority.length) && formatter.priority))) {
258 bestResult = formatter;
259 }
260 }
261 });
262
263 return bestResult ? bestResult.formatting : undefined;
264 }
265}
266
267/**
268 * The {@link LabelProvider} determines how elements/nodes are displayed in the workbench. For any element, it can determine a short label, a long label
269 * and an icon. The {@link LabelProvider} is to be used in lists, trees and tables, but also view specific locations like headers.
270 * The common {@link LabelProvider} can be extended/adapted via {@link LabelProviderContribution}s. For every element, the {@links LabelProvider} will determine the
271 * {@link LabelProviderContribution} with the hightest priority and delegate to it. Theia registers default {@link LabelProviderContribution} for common types, e.g.
272 * the {@link DefaultUriLabelProviderContribution} for elements that have a URI.
273 * Using the {@link LabelProvider} across the workbench ensures a common look and feel for elements across multiple views. To adapt the way how specific
274 * elements/nodes are rendered, use a {@link LabelProviderContribution} rather than adapting or sub classing the {@link LabelProvider}. This way, your adaptation
275 * is applied to all views in Theia that use the {@link LabelProvider}
276 */
277@injectable()
278export class LabelProvider implements FrontendApplicationContribution {
279
280 protected readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
281
282 @inject(ContributionProvider) @named(LabelProviderContribution)
283 protected readonly contributionProvider: ContributionProvider<LabelProviderContribution>;
284
285 /**
286 * Start listening to contributions.
287 *
288 * Don't call this method directly!
289 * It's called by the frontend application during initialization.
290 */
291 initialize(): void {
292 const contributions = this.contributionProvider.getContributions();
293 for (const eventContribution of contributions) {
294 if (eventContribution.onDidChange) {
295 eventContribution.onDidChange(event => {
296 this.onDidChangeEmitter.fire({
297 // TODO check eventContribution.canHandle as well
298 affects: element => this.affects(element, event)
299 });
300 });
301 }
302 }
303 }
304
305 protected affects(element: object, event: DidChangeLabelEvent): boolean {
306 if (event.affects(element)) {
307 return true;
308 }
309 for (const contribution of this.findContribution(element)) {
310 if (contribution.affects && contribution.affects(element, event)) {
311 return true;
312 }
313 }
314 return false;
315 }
316
317 get onDidChange(): Event<DidChangeLabelEvent> {
318 return this.onDidChangeEmitter.event;
319 }
320
321 /**
322 * Return a default file icon for the current icon theme.
323 */
324 get fileIcon(): string {
325 return this.getIcon(URIIconReference.create('file'));
326 }
327
328 /**
329 * Return a default folder icon for the current icon theme.
330 */
331 get folderIcon(): string {
332 return this.getIcon(URIIconReference.create('folder'));
333 }
334
335 /**
336 * Get the icon class from the list of available {@link LabelProviderContribution} for the given element.
337 * @return the icon class
338 */
339 getIcon(element: object): string {
340 return this.handleRequest(element, 'getIcon') ?? '';
341 }
342
343 /**
344 * Get a short name from the list of available {@link LabelProviderContribution} for the given element.
345 * @return the short name
346 */
347 getName(element: object): string {
348 return this.handleRequest(element, 'getName') ?? '<unknown>';
349 }
350
351 /**
352 * Get a long name from the list of available {@link LabelProviderContribution} for the given element.
353 * @return the long name
354 */
355 getLongName(element: object): string {
356 return this.handleRequest(element, 'getLongName') ?? '';
357 }
358
359 /**
360 * Get details from the list of available {@link LabelProviderContribution} for the given element.
361 * @return the details
362 * Can be used to supplement {@link getName} in contexts that allow both a primary display field and extra detail.
363 */
364 getDetails(element: object): string {
365 return this.handleRequest(element, 'getDetails') ?? '';
366 }
367
368 protected handleRequest(element: object, method: keyof Omit<LabelProviderContribution, 'canHandle' | 'onDidChange' | 'affects'>): string | undefined {
369 for (const contribution of this.findContribution(element, method)) {
370 const value = contribution[method]?.(element);
371 if (value !== undefined) {
372 return value;
373 }
374 }
375 }
376
377 protected findContribution(element: object, method?: keyof Omit<LabelProviderContribution, 'canHandle' | 'onDidChange' | 'affects'>): LabelProviderContribution[] {
378 const candidates = method
379 ? this.contributionProvider.getContributions().filter(candidate => candidate[method])
380 : this.contributionProvider.getContributions();
381 return Prioritizeable.prioritizeAllSync(candidates, contrib =>
382 contrib.canHandle(element)
383 ).map(entry => entry.value);
384 }
385}