UNPKG

9.03 kBTypeScriptView Raw
1// *****************************************************************************
2// Copyright (C) 2020 SAP SE or an SAP affiliate company 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
17/*---------------------------------------------------------------------------------------------
18* Copyright (c) Microsoft Corporation. All rights reserved.
19* Licensed under the MIT License. See License.txt in the project root for license information.
20*--------------------------------------------------------------------------------------------*/
21// some code is copied and modified from: https://github.com/microsoft/vscode/blob/573e5145ae3b50523925a6f6315d373e649d1b06/src/vs/base/common/linkedText.ts
22
23import React = require('react');
24import { inject, injectable } from 'inversify';
25import { CommandRegistry } from '../../common';
26import { ContextKeyService } from '../context-key-service';
27import { TreeModel } from './tree-model';
28import { TreeWidget } from './tree-widget';
29import { WindowService } from '../window/window-service';
30
31interface ViewWelcome {
32 readonly view: string;
33 readonly content: string;
34 readonly when?: string;
35 readonly order: number;
36}
37
38interface IItem {
39 readonly welcomeInfo: ViewWelcome;
40 visible: boolean;
41}
42
43interface ILink {
44 readonly label: string;
45 readonly href: string;
46 readonly title?: string;
47}
48
49type LinkedTextItem = string | ILink;
50
51@injectable()
52export class TreeViewWelcomeWidget extends TreeWidget {
53
54 @inject(CommandRegistry)
55 protected readonly commands: CommandRegistry;
56
57 @inject(ContextKeyService)
58 protected readonly contextService: ContextKeyService;
59
60 @inject(WindowService)
61 protected readonly windowService: WindowService;
62
63 protected viewWelcomeNodes: React.ReactNode[] = [];
64 protected defaultItem: IItem | undefined;
65 protected items: IItem[] = [];
66 get visibleItems(): ViewWelcome[] {
67 const visibleItems = this.items.filter(v => v.visible);
68 if (visibleItems.length && this.defaultItem) {
69 return [this.defaultItem.welcomeInfo];
70 }
71 return visibleItems.map(v => v.welcomeInfo);
72 }
73
74 protected override renderTree(model: TreeModel): React.ReactNode {
75 if (this.shouldShowWelcomeView() && this.visibleItems.length) {
76 return this.renderViewWelcome();
77 }
78 return super.renderTree(model);
79 }
80
81 protected shouldShowWelcomeView(): boolean {
82 return false;
83 }
84
85 protected renderViewWelcome(): React.ReactNode {
86 return (
87 <div className='theia-WelcomeView'>
88 {...this.viewWelcomeNodes}
89 </div>
90 );
91 }
92
93 handleViewWelcomeContentChange(viewWelcomes: ViewWelcome[]): void {
94 this.items = [];
95 for (const welcomeInfo of viewWelcomes) {
96 if (welcomeInfo.when === 'default') {
97 this.defaultItem = { welcomeInfo, visible: true };
98 } else {
99 const visible = welcomeInfo.when === undefined || this.contextService.match(welcomeInfo.when);
100 this.items.push({ welcomeInfo, visible });
101 }
102 }
103 this.updateViewWelcomeNodes();
104 this.update();
105 }
106
107 handleWelcomeContextChange(): void {
108 let didChange = false;
109
110 for (const item of this.items) {
111 if (!item.welcomeInfo.when || item.welcomeInfo.when === 'default') {
112 continue;
113 }
114
115 const visible = item.welcomeInfo.when === undefined || this.contextService.match(item.welcomeInfo.when);
116
117 if (item.visible === visible) {
118 continue;
119 }
120
121 item.visible = visible;
122 didChange = true;
123 }
124
125 if (didChange) {
126 this.updateViewWelcomeNodes();
127 this.update();
128 }
129 }
130
131 protected updateViewWelcomeNodes(): void {
132 this.viewWelcomeNodes = [];
133 const items = this.visibleItems.sort((a, b) => a.order - b.order);
134
135 for (const [iIndex, { content }] of items.entries()) {
136 const lines = content.split('\n');
137
138 for (let [lIndex, line] of lines.entries()) {
139 const lineKey = `${iIndex}-${lIndex}`;
140 line = line.trim();
141
142 if (!line) {
143 continue;
144 }
145
146 const linkedTextItems = this.parseLinkedText(line);
147
148 if (linkedTextItems.length === 1 && typeof linkedTextItems[0] !== 'string') {
149 this.viewWelcomeNodes.push(
150 this.renderButtonNode(linkedTextItems[0], lineKey)
151 );
152 } else {
153 const linkedTextNodes: React.ReactNode[] = [];
154
155 for (const [nIndex, node] of linkedTextItems.entries()) {
156 const linkedTextKey = `${lineKey}-${nIndex}`;
157
158 if (typeof node === 'string') {
159 linkedTextNodes.push(
160 this.renderTextNode(node, linkedTextKey)
161 );
162 } else {
163 linkedTextNodes.push(
164 this.renderCommandLinkNode(node, linkedTextKey)
165 );
166 }
167 }
168
169 this.viewWelcomeNodes.push(
170 <div key={`line-${lineKey}`}>
171 {...linkedTextNodes}
172 </div>
173 );
174 }
175 }
176 }
177 }
178
179 protected renderButtonNode(node: ILink, lineKey: string): React.ReactNode {
180 return (
181 <div key={`line-${lineKey}`} className='theia-WelcomeViewButtonWrapper'>
182 <button title={node.title}
183 className='theia-button theia-WelcomeViewButton'
184 disabled={!this.isEnabledClick(node.href)}
185 onClick={e => this.openLinkOrCommand(e, node.href)}>
186 {node.label}
187 </button>
188 </div>
189 );
190 }
191
192 protected renderTextNode(node: string, textKey: string): React.ReactNode {
193 return <span key={`text-${textKey}`}>{node}</span>;
194 }
195
196 protected renderCommandLinkNode(node: ILink, linkKey: string): React.ReactNode {
197 return (
198 <a key={`link-${linkKey}`}
199 className={this.getLinkClassName(node.href)}
200 title={node.title || ''}
201 onClick={e => this.openLinkOrCommand(e, node.href)}>
202 {node.label}
203 </a>
204 );
205 }
206
207 protected getLinkClassName(href: string): string {
208 const classNames = ['theia-WelcomeViewCommandLink'];
209 if (!this.isEnabledClick(href)) {
210 classNames.push('disabled');
211 }
212 return classNames.join(' ');
213 }
214
215 protected isEnabledClick(href: string): boolean {
216 if (href.startsWith('command:')) {
217 const command = href.replace('command:', '');
218 return this.commands.isEnabled(command);
219 }
220 return true;
221 }
222
223 protected openLinkOrCommand = (event: React.MouseEvent, href: string): void => {
224 event.stopPropagation();
225
226 if (href.startsWith('command:')) {
227 const command = href.replace('command:', '');
228 this.commands.executeCommand(command);
229 } else {
230 this.windowService.openNewWindow(href, { external: true });
231 }
232 };
233
234 protected parseLinkedText(text: string): LinkedTextItem[] {
235 const result: LinkedTextItem[] = [];
236
237 const linkRegex = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: ("|')([^\3]+)(\3))?\)/gi;
238 let index = 0;
239 let match: RegExpExecArray | null;
240
241 while (match = linkRegex.exec(text)) {
242 if (match.index - index > 0) {
243 result.push(text.substring(index, match.index));
244 }
245
246 const [, label, href, , title] = match;
247
248 if (title) {
249 result.push({ label, href, title });
250 } else {
251 result.push({ label, href });
252 }
253
254 index = match.index + match[0].length;
255 }
256
257 if (index < text.length) {
258 result.push(text.substring(index));
259 }
260
261 return result;
262 }
263}