UNPKG

7.78 kBJavaScriptView Raw
1import { from, Subject } from 'rxjs';
2import { filter, map, mergeMap } from 'rxjs/operators';
3import { Controller as BaseController } from 'sourcegraph/module/client/controller';
4import { MessageType } from 'sourcegraph/module/protocol';
5import { BrowserConsoleTracer, Trace } from 'sourcegraph/module/protocol/jsonrpc2/trace';
6import { ExtensionStatus } from '../app/ExtensionStatus';
7import { asError, isErrorLike } from '../errors';
8import { isExtensionEnabled } from '../extensions/extension';
9import { registerBuiltinClientCommands, updateConfiguration } from './clientCommands';
10import { log } from './log';
11/**
12 * Extends the {@link BaseController} class to add functionality that is useful to this package's consumers.
13 */
14export class Controller extends BaseController {
15 constructor() {
16 super(...arguments);
17 /**
18 * Global notification messages that should be displayed to the user, from the following sources:
19 *
20 * - window/showMessage notifications from extensions
21 * - Errors thrown or returned in command invocation
22 */
23 this.notifications = new Subject();
24 }
25 /**
26 * Executes the command (registered in the CommandRegistry) specified in params. If an error is thrown, the
27 * error is returned *and* emitted on the {@link Controller#notifications} observable.
28 *
29 * All callers should execute commands using this method instead of calling
30 * {@link sourcegraph:CommandRegistry#executeCommand} directly (to ensure errors are
31 * emitted as notifications).
32 */
33 executeCommand(params) {
34 return this.registries.commands.executeCommand(params).catch(err => {
35 this.notifications.next({ message: err, type: MessageType.Error, source: params.command });
36 return Promise.reject(err);
37 });
38 }
39}
40/**
41 * Filter the environment to omit extensions that should not be activated (based on their manifest's
42 * activationEvents).
43 *
44 * @template CC configuration cascade type
45 */
46function environmentFilter(nextEnvironment) {
47 return Object.assign({}, nextEnvironment, { extensions: nextEnvironment.extensions &&
48 nextEnvironment.extensions.filter(x => {
49 try {
50 if (!isExtensionEnabled(nextEnvironment.configuration.merged, x.id)) {
51 return false;
52 }
53 else if (!x.manifest) {
54 console.warn(`Extension ${x.id} was not found. Remove it from settings to suppress this warning.`);
55 return false;
56 }
57 else if (isErrorLike(x.manifest)) {
58 console.warn(asError(x.manifest));
59 return false;
60 }
61 else if (!x.manifest.activationEvents) {
62 console.warn(`Extension ${x.id} has no activation events, so it will never be activated.`);
63 return false;
64 }
65 const visibleTextDocumentLanguages = nextEnvironment.visibleTextDocuments
66 ? nextEnvironment.visibleTextDocuments.map(({ languageId }) => languageId)
67 : [];
68 return x.manifest.activationEvents.some(e => e === '*' || visibleTextDocumentLanguages.some(l => e === `onLanguage:${l}`));
69 }
70 catch (err) {
71 console.error(err);
72 }
73 return false;
74 }) });
75}
76/**
77 * Creates the controller, which handles all communication between the React app and Sourcegraph extensions.
78 *
79 * There should only be a single controller for the entire application. The controller's environment represents all
80 * of the application state that the controller needs to know.
81 *
82 * It receives state updates via calls to the setEnvironment method from React components. It provides results to
83 * React components via its registries and the showMessages, etc., observables.
84 */
85export function createController(context, createMessageTransports) {
86 const controller = new Controller({
87 clientOptions: (_key, extension) => ({
88 createMessageTransports: () => createMessageTransports(extension),
89 }),
90 environmentFilter,
91 });
92 // Apply trace settings.
93 //
94 // HACK(sqs): This is inefficient and doesn't unsubscribe itself.
95 controller.clientEntries.subscribe(entries => {
96 const traceEnabled = localStorage.getItem(ExtensionStatus.TRACE_STORAGE_KEY) !== null;
97 for (const e of entries) {
98 e.connection
99 .then(c => c.trace(traceEnabled ? Trace.Verbose : Trace.Off, new BrowserConsoleTracer(e.key.id)))
100 .catch(err => console.error(err));
101 }
102 });
103 registerBuiltinClientCommands(context, controller);
104 registerExtensionContributions(controller);
105 // Show messages (that don't need user input) as global notifications.
106 controller.showMessages.subscribe(({ message, type }) => controller.notifications.next({ message, type }));
107 function messageFromExtension(message) {
108 return `From extension:\n\n${message}`;
109 }
110 controller.showMessageRequests.subscribe(({ message, actions, resolve }) => {
111 if (!actions || actions.length === 0) {
112 alert(messageFromExtension(message));
113 resolve(null);
114 return;
115 }
116 const value = prompt(messageFromExtension(`${message}\n\nValid responses: ${actions.map(({ title }) => JSON.stringify(title)).join(', ')}`), actions[0].title);
117 resolve(actions.find(a => a.title === value) || null);
118 });
119 controller.showInputs.subscribe(({ message, defaultValue, resolve }) => resolve(prompt(messageFromExtension(message), defaultValue)));
120 controller.configurationUpdates
121 .pipe(mergeMap(params => {
122 const update = updateConfiguration(context, params);
123 params.resolve(update);
124 return from(update);
125 }))
126 .subscribe(undefined, err => console.error(err));
127 // Print window/logMessage log messages to the browser devtools console.
128 controller.logMessages.subscribe(({ message }) => {
129 log('info', 'EXT', message);
130 });
131 // Debug helpers.
132 const DEBUG = true;
133 if (DEBUG) {
134 // Debug helper: log environment changes.
135 const LOG_ENVIRONMENT = false;
136 if (LOG_ENVIRONMENT) {
137 controller.environment.subscribe(environment => log('info', 'env', environment));
138 }
139 // Debug helpers: e.g., just run `sx` in devtools to get a reference to this controller. (If multiple
140 // controllers are created, this points to the last one created.)
141 window.sx = controller;
142 // This value is synchronously available because observable has an underlying
143 // BehaviorSubject source.
144 controller.environment.subscribe(v => (window.sxenv = v)).unsubscribe();
145 }
146 return controller;
147}
148/**
149 * Registers the builtin client commands that are required by Sourcegraph extensions. See
150 * {@link module:sourcegraph.module/protocol/contribution.ActionContribution#command} for
151 * documentation.
152 */
153function registerExtensionContributions(controller) {
154 const contributions = controller.environment.pipe(map(({ extensions }) => extensions), filter((extensions) => !!extensions), map(extensions => extensions
155 .map(({ manifest }) => manifest)
156 .filter((manifest) => manifest !== null && !isErrorLike(manifest))
157 .map(({ contributes }) => contributes)
158 .filter((contributions) => !!contributions)));
159 return controller.registries.contribution.registerContributions({
160 contributions,
161 });
162}
163//# sourceMappingURL=controller.js.map
\No newline at end of file