@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
233 lines (201 loc) • 6.28 kB
text/typescript
import '../../style/console.css';
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { Signal } from '@lumino/signaling';
import {
LanguageServers as LSPSettings,
LoggingConsoleVerbosityLevel
} from '../_plugin';
import { ILSPLogConsole, ILogConsoleCore, PLUGIN_ID } from '../tokens';
interface ILogConsoleImplementation extends ILogConsoleCore {
dispose(): void;
bindThis(): any;
}
function toString(args: any[]): string {
return args
.map(arg => {
let textual: string;
if (typeof arg === 'string') {
return arg;
}
try {
textual = JSON.stringify({ k: arg }).slice(5, -1);
} catch {
textual = '' + arg;
}
return '<span class="lsp-code">' + textual + '</span>';
})
.join(' ');
}
class FloatingConsole implements ILogConsoleImplementation {
// likely to be replaced by JupyterLab console: https://github.com/jupyterlab/jupyterlab/pull/6833#issuecomment-543016425
private readonly element: HTMLElement;
constructor(scope: string[] = ['LSP'], element: HTMLElement | null = null) {
if (element) {
this.element = element;
} else {
this.element = document.createElement('ul');
this.element.className = 'lsp-floating-console';
document.body.appendChild(this.element);
}
}
bindThis(): any {
return this;
}
dispose() {
this.element.remove();
}
append(text: string, kind = 'log') {
let entry = document.createElement('li');
entry.innerHTML = '<span class="lsp-kind">' + kind + '</span>' + text;
this.element.appendChild(entry);
this.element.scrollTop = this.element.scrollHeight;
}
debug(...args: any[]) {
this.append(toString(args), 'debug');
}
log(...args: any[]) {
this.append(toString(args), 'log');
}
warn(...args: any[]) {
this.append(toString(args), 'warn');
}
error(...args: any[]) {
this.append(toString(args), 'error');
}
}
/**
* Used both as a console implementation, and as a dummy ILSPLogConsole
*/
export class BrowserConsole
implements ILogConsoleImplementation, ILSPLogConsole
{
debug = window.console.debug.bind(window.console);
log = window.console.log.bind(window.console);
warn = window.console.warn.bind(window.console);
error = window.console.error.bind(window.console);
dispose(): void {
return;
}
scope(scope: string): BrowserConsole {
return this;
}
bindThis(): any {
return window.console;
}
}
class ConsoleWrapper implements ILSPLogConsole {
private readonly singleton: ConsoleSingleton;
private readonly breadcrumbs: string[];
private readonly _scope: string;
constructor(scope: string[] = ['LSP'], singleton: ConsoleSingleton) {
this.breadcrumbs = scope;
this._scope = this.breadcrumbs.join('.') + ':';
this.singleton = singleton;
this.singleton.changed.connect(this._rebindFunctions.bind(this));
this._rebindFunctions();
}
/**
* Re-binding directly to the underlying functions allows to get nice
* location (file:line) of the actual call rather than of this wrapper
* when using with the browser implementation.
*/
_rebindFunctions() {
const noOp = () => {
// do nothing
};
const bindArg = this.implementation.bindThis();
if (this.verbosity === 'debug') {
this.debug = this.implementation.debug.bind(bindArg, this._scope);
} else {
this.debug = noOp;
}
if (this.verbosity === 'debug' || this.verbosity === 'log') {
this.log = this.implementation.log.bind(bindArg, this._scope);
} else {
this.log = noOp;
}
if (
this.verbosity === 'debug' ||
this.verbosity === 'log' ||
this.verbosity === 'warn'
) {
this.warn = this.implementation.warn.bind(bindArg, this._scope);
} else {
this.warn = noOp;
}
this.error = this.implementation.error.bind(bindArg, this._scope);
}
get verbosity() {
return this.singleton.verbosity;
}
get implementation() {
return this.singleton.implementation;
}
debug(...args: any[]) {
return this.implementation.debug(this._scope, ...args);
}
log(...args: any[]) {
return this.implementation.log(this._scope, ...args);
}
warn(...args: any[]) {
return this.implementation.warn(this._scope, ...args);
}
error(...args: any[]) {
return this.implementation.error(this._scope, ...args);
}
scope(scope: string): ILSPLogConsole {
return new ConsoleWrapper([...this.breadcrumbs, scope], this.singleton);
}
}
class ConsoleSingleton {
public verbosity: LoggingConsoleVerbosityLevel;
public implementation: ILogConsoleImplementation;
public changed: Signal<ConsoleSingleton, void>;
constructor(settingRegistry: ISettingRegistry) {
// until loaded log everything, to browser console
this.verbosity = 'debug';
this.implementation = new BrowserConsole();
this.changed = new Signal(this);
settingRegistry
.load(PLUGIN_ID + ':plugin')
.then(settings => {
this.updateSettings(settings);
settings.changed.connect(() => {
this.updateSettings(settings);
});
})
.catch(console.warn);
}
updateSettings(settings: ISettingRegistry.ISettings) {
const composite = settings.composite as LSPSettings;
const kind = composite.loggingConsole;
if (this.implementation) {
this.implementation.dispose();
}
if (kind === 'browser') {
this.implementation = new BrowserConsole();
} else if (kind === 'floating') {
this.implementation = new FloatingConsole();
} else {
console.warn(
'Unknown console type',
kind,
'falling back to browser console'
);
this.implementation = new BrowserConsole();
}
this.verbosity = composite.loggingLevel!;
this.changed.emit();
}
}
export const LOG_CONSOLE: JupyterFrontEndPlugin<ILSPLogConsole> = {
id: PLUGIN_ID + ':ILSPLogConsole',
requires: [ISettingRegistry],
activate: (app, settingRegistry: ISettingRegistry) => {
const singleton = new ConsoleSingleton(settingRegistry);
return new ConsoleWrapper(['LSP'], singleton);
},
provides: ILSPLogConsole,
autoStart: true
};