Skip to content

Commit 5f869fd

Browse files
committed
Adds app component to eventually replace appBase
Adds ipc & logging context for components
1 parent 803db7f commit 5f869fd

File tree

8 files changed

+238
-35
lines changed

8 files changed

+238
-35
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17981,6 +17981,7 @@
1798117981
"@gitkraken/gitkraken-components": "10.6.0",
1798217982
"@gitkraken/provider-apis": "0.24.2",
1798317983
"@gitkraken/shared-web-components": "0.1.1-rc.15",
17984+
"@lit/context": "1.1.2",
1798417985
"@lit/react": "1.0.5",
1798517986
"@microsoft/fast-element": "1.13.0",
1798617987
"@octokit/graphql": "8.1.1",

src/system/logger.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LogInstanceNameFn } from './decorators/log';
22
import type { LogLevel } from './logger.constants';
33
import type { LogScope } from './logger.scope';
4+
import { padOrTruncateEnd } from './string';
45

56
const enum OrderedLevel {
67
Off = 0,
@@ -87,7 +88,7 @@ export const Logger = new (class Logger {
8788
}
8889

8990
if (this.isDebugging) {
90-
console.log(this.timestamp, `[${this.provider!.name}]`, message ?? '', ...params);
91+
console.log(`[${padOrTruncateEnd(this.provider!.name, 13)}]`, this.timestamp, message ?? '', ...params);
9192
}
9293

9394
if (this.output == null || this.level < OrderedLevel.Debug) return;
@@ -118,9 +119,20 @@ export const Logger = new (class Logger {
118119

119120
if (this.isDebugging) {
120121
if (ex != null) {
121-
console.error(this.timestamp, `[${this.provider!.name}]`, message ?? '', ...params, ex);
122+
console.error(
123+
`[${padOrTruncateEnd(this.provider!.name, 13)}]`,
124+
this.timestamp,
125+
message ?? '',
126+
...params,
127+
ex,
128+
);
122129
} else {
123-
console.error(this.timestamp, `[${this.provider!.name}]`, message ?? '', ...params);
130+
console.error(
131+
`[${padOrTruncateEnd(this.provider!.name, 13)}]`,
132+
this.timestamp,
133+
message ?? '',
134+
...params,
135+
);
124136
}
125137
}
126138

@@ -149,7 +161,7 @@ export const Logger = new (class Logger {
149161
}
150162

151163
if (this.isDebugging) {
152-
console.log(this.timestamp, `[${this.provider!.name}]`, message ?? '', ...params);
164+
console.log(`[${padOrTruncateEnd(this.provider!.name, 13)}]`, this.timestamp, message ?? '', ...params);
153165
}
154166

155167
if (this.output == null || this.level < OrderedLevel.Info) return;

src/system/string.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,12 @@ export function pad(s: string, before: number = 0, after: number = 0, padding: s
501501
return `${before === 0 ? '' : padding.repeat(before)}${s}${after === 0 ? '' : padding.repeat(after)}`;
502502
}
503503

504+
export function padOrTruncateEnd(s: string, maxLength: number, fillString?: string) {
505+
if (s.length === maxLength) return s;
506+
if (s.length > maxLength) return s.substring(0, maxLength);
507+
return s.padEnd(maxLength, fillString);
508+
}
509+
504510
export function pluralize(
505511
s: string,
506512
count: number,

src/webviews/apps/shared/app.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { provide } from '@lit/context';
2+
import { html, LitElement } from 'lit';
3+
import { property } from 'lit/decorators.js';
4+
import type { CustomEditorIds, WebviewIds, WebviewViewIds } from '../../../constants.views';
5+
import type { Deferrable } from '../../../system/function';
6+
import { debounce } from '../../../system/function';
7+
import type { WebviewFocusChangedParams } from '../../protocol';
8+
import { DidChangeWebviewFocusNotification, WebviewFocusChangedCommand, WebviewReadyCommand } from '../../protocol';
9+
import { GlElement } from './components/element';
10+
import { ipcContext, loggerContext, LoggerContext } from './context';
11+
import type { Disposable } from './events';
12+
import { HostIpc } from './ipc';
13+
14+
export abstract class GlApp<
15+
State extends { webviewId: CustomEditorIds | WebviewIds | WebviewViewIds; timestamp: number } = {
16+
webviewId: CustomEditorIds | WebviewIds | WebviewViewIds;
17+
timestamp: number;
18+
},
19+
> extends GlElement {
20+
static override shadowRootOptions: ShadowRootInit = {
21+
...LitElement.shadowRootOptions,
22+
delegatesFocus: true,
23+
};
24+
25+
@property({ type: String }) name!: string;
26+
@property({ type: String }) placement: 'editor' | 'view' = 'editor';
27+
28+
@provide({ context: ipcContext })
29+
protected _ipc!: HostIpc;
30+
31+
@provide({ context: loggerContext })
32+
protected _logger!: LoggerContext;
33+
34+
@property({ type: Object })
35+
state!: State;
36+
37+
private readonly disposables: Disposable[] = [];
38+
private _focused?: boolean;
39+
private _inputFocused?: boolean;
40+
private _sendWebviewFocusChangedCommandDebounced!: Deferrable<(params: WebviewFocusChangedParams) => void>;
41+
42+
override connectedCallback() {
43+
super.connectedCallback();
44+
45+
this._logger = new LoggerContext(this.name);
46+
this._logger.log('connected');
47+
48+
//const themeEvent = computeThemeColors();
49+
//if (this.onThemeUpdated != null) {
50+
//this.onThemeUpdated(themeEvent);
51+
//disposables.push(onDidChangeTheme(this.onThemeUpdated, this));
52+
//}
53+
54+
this._ipc = new HostIpc(this.name);
55+
this.disposables.push(
56+
this._ipc.onReceiveMessage(msg => {
57+
switch (true) {
58+
case DidChangeWebviewFocusNotification.is(msg):
59+
window.dispatchEvent(new CustomEvent(msg.params.focused ? 'webview-focus' : 'webview-blur'));
60+
break;
61+
}
62+
}),
63+
this._ipc,
64+
);
65+
this._ipc.sendCommand(WebviewReadyCommand, undefined);
66+
67+
this._sendWebviewFocusChangedCommandDebounced = debounce((params: WebviewFocusChangedParams) => {
68+
this._ipc.sendCommand(WebviewFocusChangedCommand, params);
69+
}, 150);
70+
71+
// Removes VS Code's default title attributes on <a> tags
72+
document.querySelectorAll('a').forEach(a => {
73+
if (a.href === a.title) {
74+
a.removeAttribute('title');
75+
}
76+
});
77+
78+
document.addEventListener('focusin', this.onFocusIn);
79+
document.addEventListener('focusout', this.onFocusOut);
80+
81+
if (document.body.classList.contains('preload')) {
82+
setTimeout(() => {
83+
document.body.classList.remove('preload');
84+
}, 500);
85+
}
86+
}
87+
88+
override disconnectedCallback() {
89+
super.disconnectedCallback();
90+
91+
this._logger.log('disconnected');
92+
93+
document.removeEventListener('focusin', this.onFocusIn);
94+
document.removeEventListener('focusout', this.onFocusOut);
95+
this.disposables.forEach(d => d.dispose());
96+
}
97+
98+
override render() {
99+
return html`<slot></slot>`;
100+
}
101+
102+
private onFocusIn = (e: FocusEvent) => {
103+
const inputFocused = e.composedPath().some(el => (el as HTMLElement).tagName === 'INPUT');
104+
105+
if (this._focused !== true || this._inputFocused !== inputFocused) {
106+
this._focused = true;
107+
this._inputFocused = inputFocused;
108+
this._sendWebviewFocusChangedCommandDebounced({ focused: true, inputFocused: inputFocused });
109+
}
110+
};
111+
112+
private onFocusOut = (_e: FocusEvent) => {
113+
if (this._focused !== false || this._inputFocused !== false) {
114+
this._focused = false;
115+
this._inputFocused = false;
116+
this._sendWebviewFocusChangedCommandDebounced({ focused: false, inputFocused: false });
117+
}
118+
};
119+
}

src/webviews/apps/shared/appBase.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*global window document*/
2+
import { ContextProvider } from '@lit/context';
23
import type { CustomEditorIds, WebviewIds, WebviewViewIds } from '../../../constants.views';
34
import { debounce } from '../../../system/function';
4-
import { Logger } from '../../../system/logger';
55
import type { LogScope } from '../../../system/logger.scope';
66
import type {
77
IpcCallParamsType,
@@ -12,15 +12,14 @@ import type {
1212
WebviewFocusChangedParams,
1313
} from '../../protocol';
1414
import { DidChangeWebviewFocusNotification, WebviewFocusChangedCommand, WebviewReadyCommand } from '../../protocol';
15+
import { ipcContext, loggerContext, LoggerContext } from './context';
1516
import { DOM } from './dom';
1617
import type { Disposable } from './events';
1718
import type { HostIpcApi } from './ipc';
1819
import { getHostIpcApi, HostIpc } from './ipc';
1920
import type { ThemeChangeEvent } from './theme';
2021
import { computeThemeColors, onDidChangeTheme, watchThemeColors } from './theme';
2122

22-
declare const DEBUG: boolean;
23-
2423
export abstract class App<
2524
State extends { webviewId: CustomEditorIds | WebviewIds | WebviewViewIds; timestamp: number } = {
2625
webviewId: CustomEditorIds | WebviewIds | WebviewViewIds;
@@ -29,6 +28,7 @@ export abstract class App<
2928
> {
3029
private readonly _api: HostIpcApi;
3130
private readonly _hostIpc: HostIpc;
31+
private readonly _logger: LoggerContext;
3232

3333
protected state: State;
3434
protected readonly placement: 'editor' | 'view';
@@ -47,28 +47,19 @@ export abstract class App<
4747

4848
this.placement = (document.body.getAttribute('data-placement') ?? 'editor') as 'editor' | 'view';
4949

50-
Logger.configure(
51-
{
52-
name: appName,
53-
createChannel: function (name: string) {
54-
return {
55-
name: name,
56-
appendLine: function (value: string) {
57-
console.log(`[${name}] ${value}`);
58-
},
59-
};
60-
},
61-
},
62-
DEBUG ? 'debug' : 'off',
63-
);
64-
65-
this.log(`${appName}()`);
66-
// this.log(`ctor(${this.state ? JSON.stringify(this.state) : ''})`);
50+
this._logger = new LoggerContext(appName);
51+
this.log('opening...');
6752

6853
this._api = getHostIpcApi();
6954
this._hostIpc = new HostIpc(this.appName);
7055
disposables.push(this._hostIpc);
7156

57+
new ContextProvider(document.body, { context: ipcContext, initialValue: this._hostIpc });
58+
new ContextProvider(document.body, {
59+
context: loggerContext,
60+
initialValue: this._logger,
61+
});
62+
7263
if (this.state != null) {
7364
const state = this.getState();
7465
if (this.state.timestamp >= (state?.timestamp ?? 0)) {
@@ -81,7 +72,7 @@ export abstract class App<
8172
disposables.push(watchThemeColors());
8273

8374
requestAnimationFrame(() => {
84-
this.log(`${appName}(): initializing...`);
75+
this.log('initializing...');
8576

8677
try {
8778
this.onInitialize?.();
@@ -108,6 +99,7 @@ export abstract class App<
10899

109100
this.onInitialized?.();
110101
} finally {
102+
this.log('initialized');
111103
if (document.body.classList.contains('preload')) {
112104
setTimeout(() => {
113105
document.body.classList.remove('preload');
@@ -123,6 +115,8 @@ export abstract class App<
123115
this.bindDisposables = undefined;
124116
}),
125117
);
118+
119+
this.log('opened');
126120
}
127121

128122
protected onInitialize?(): void;
@@ -176,11 +170,7 @@ export abstract class App<
176170
protected log(message: string, ...optionalParams: any[]): void;
177171
protected log(scope: LogScope | undefined, message: string, ...optionalParams: any[]): void;
178172
protected log(scopeOrMessage: LogScope | string | undefined, ...optionalParams: any[]): void {
179-
if (typeof scopeOrMessage === 'string') {
180-
Logger.log(scopeOrMessage, ...optionalParams);
181-
} else {
182-
Logger.log(scopeOrMessage, optionalParams.shift(), ...optionalParams);
183-
}
173+
this._logger.log(scopeOrMessage, ...optionalParams);
184174
}
185175

186176
protected getState(): State | undefined {

src/webviews/apps/shared/context.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createContext } from '@lit/context';
2+
import { Logger } from '../../../system/logger';
3+
import type { LogScope } from '../../../system/logger.scope';
4+
import { getNewLogScope } from '../../../system/logger.scope';
5+
import { padOrTruncateEnd } from '../../../system/string';
6+
import type { HostIpc } from './ipc';
7+
8+
declare const DEBUG: boolean;
9+
10+
export class LoggerContext {
11+
private readonly scope: LogScope;
12+
13+
constructor(appName: string) {
14+
this.scope = getNewLogScope(appName, undefined);
15+
Logger.configure(
16+
{
17+
name: appName,
18+
createChannel: function (name: string) {
19+
return {
20+
name: name,
21+
appendLine: function (value: string) {
22+
console.log(`[${padOrTruncateEnd(name, 13)}] ${value}`);
23+
},
24+
};
25+
},
26+
},
27+
DEBUG ? 'debug' : 'off',
28+
);
29+
}
30+
31+
log(messageOrScope: string | LogScope | undefined, ...optionalParams: any[]): void {
32+
if (typeof messageOrScope === 'string') {
33+
Logger.log(this.scope, messageOrScope, ...optionalParams);
34+
} else {
35+
Logger.log(messageOrScope, optionalParams.shift(), ...optionalParams);
36+
}
37+
}
38+
}
39+
40+
export const ipcContext = createContext<HostIpc>('ipc');
41+
export const loggerContext = createContext<LoggerContext>('logger');

src/webviews/webviewController.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,13 +659,15 @@ export function replaceWebviewHtmlTokens<SerializedState>(
659659
endOfBody?: string,
660660
) {
661661
return html.replace(
662-
/#{(head|body|endOfBody|webviewId|webviewInstanceId|placement|cspSource|cspNonce|root|webroot)}/g,
662+
/#{(head|body|endOfBody|webviewId|webviewInstanceId|placement|cspSource|cspNonce|root|webroot|state)}/g,
663663
(_substring: string, token: string) => {
664664
switch (token) {
665665
case 'head':
666666
return head ?? '';
667667
case 'body':
668668
return body ?? '';
669+
case 'state':
670+
return bootstrap != null ? JSON.stringify(bootstrap).replace(/"/g, '&quot;') : '';
669671
case 'endOfBody':
670672
return `${
671673
bootstrap != null

0 commit comments

Comments
 (0)