1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { inject, injectable, named, postConstruct } from 'inversify';
|
18 | import * as fileIcons from 'file-icons-js';
|
19 | import URI from '../common/uri';
|
20 | import { ContributionProvider } from '../common/contribution-provider';
|
21 | import { Event, Emitter, Disposable, isObject, Path, Prioritizeable } from '../common';
|
22 | import { FrontendApplicationContribution } from './frontend-application';
|
23 | import { EnvVariablesServer } from '../common/env-variables/env-variables-protocol';
|
24 | import { ResourceLabelFormatter, ResourceLabelFormatting } from '../common/label-protocol';
|
25 | import { codicon } from './widgets';
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | const DEFAULT_FOLDER_ICON = `${codicon('folder')} default-folder-icon`;
|
31 |
|
32 |
|
33 |
|
34 | const DEFAULT_FILE_ICON = `${codicon('file')} default-file-icon`;
|
35 |
|
36 | export const LabelProviderContribution = Symbol('LabelProviderContribution');
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | export interface LabelProviderContribution {
|
47 |
|
48 | |
49 |
|
50 |
|
51 |
|
52 |
|
53 | canHandle(element: object): number;
|
54 |
|
55 | |
56 |
|
57 |
|
58 | getIcon?(element: object): string | undefined;
|
59 |
|
60 | |
61 |
|
62 |
|
63 | getName?(element: object): string | undefined;
|
64 |
|
65 | |
66 |
|
67 |
|
68 | getLongName?(element: object): string | undefined;
|
69 |
|
70 | |
71 |
|
72 |
|
73 | getDetails?(element: object): string | undefined;
|
74 |
|
75 | |
76 |
|
77 |
|
78 |
|
79 | readonly onDidChange?: Event<DidChangeLabelEvent>;
|
80 |
|
81 | |
82 |
|
83 |
|
84 |
|
85 |
|
86 | affects?(element: object, event: DidChangeLabelEvent): boolean;
|
87 |
|
88 | }
|
89 |
|
90 | export interface DidChangeLabelEvent {
|
91 | affects(element: object): boolean;
|
92 | }
|
93 |
|
94 | export interface URIIconReference {
|
95 | kind: 'uriIconReference';
|
96 | id: 'file' | 'folder';
|
97 | uri?: URI
|
98 | }
|
99 | export 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()
|
109 | export 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 |
|
207 | |
208 |
|
209 |
|
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 |
|
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 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 | @injectable()
|
278 | export 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 |
|
287 |
|
288 |
|
289 |
|
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 |
|
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 |
|
323 |
|
324 | get fileIcon(): string {
|
325 | return this.getIcon(URIIconReference.create('file'));
|
326 | }
|
327 |
|
328 | |
329 |
|
330 |
|
331 | get folderIcon(): string {
|
332 | return this.getIcon(URIIconReference.create('folder'));
|
333 | }
|
334 |
|
335 | |
336 |
|
337 |
|
338 |
|
339 | getIcon(element: object): string {
|
340 | return this.handleRequest(element, 'getIcon') ?? '';
|
341 | }
|
342 |
|
343 | |
344 |
|
345 |
|
346 |
|
347 | getName(element: object): string {
|
348 | return this.handleRequest(element, 'getName') ?? '<unknown>';
|
349 | }
|
350 |
|
351 | |
352 |
|
353 |
|
354 |
|
355 | getLongName(element: object): string {
|
356 | return this.handleRequest(element, 'getLongName') ?? '';
|
357 | }
|
358 |
|
359 | |
360 |
|
361 |
|
362 |
|
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 | }
|