@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
270 lines (246 loc) • 9.15 kB
text/typescript
/** The Public API, as exposed in the `main` field of package.json */
/** General public tokens, including lumino Tokens and namespaces */
export * from './tokens';
/** Generated JSON Schema types for server responses and settings */
export * as SCHEMA from './_schema';
/** Component- and feature-specific APIs */
export * from './api';
import { COMPLETION_THEME_MANAGER } from '@jupyter-lsp/completion-theme';
import { plugin as THEME_MATERIAL } from '@jupyter-lsp/theme-material';
import { plugin as THEME_VSCODE } from '@jupyter-lsp/theme-vscode';
import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { ILoggerRegistry } from '@jupyterlab/logconsole';
import {
ILSPDocumentConnectionManager,
DocumentConnectionManager,
ILanguageServerManager
} from '@jupyterlab/lsp';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { IStatusBar } from '@jupyterlab/statusbar';
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
import { IFormRendererRegistry } from '@jupyterlab/ui-components';
import { JSONExt } from '@lumino/coreutils';
import '../style/index.css';
import { LanguageServers } from './_plugin';
import { FILEEDITOR_ADAPTER_PLUGIN } from './adapters/fileeditor';
import { NOTEBOOK_ADAPTER_PLUGIN } from './adapters/notebook';
import { StatusButtonExtension } from './components/statusbar';
import {
COMPLETION_PLUGIN,
COMPLETION_FALLBACK_PLUGIN
} from './features/completion';
import { DIAGNOSTICS_PLUGIN } from './features/diagnostics';
import { HIGHLIGHTS_PLUGIN } from './features/highlights';
import { HOVER_PLUGIN } from './features/hover';
import { JUMP_PLUGIN } from './features/jump_to';
import { RENAME_PLUGIN } from './features/rename';
import { SIGNATURE_PLUGIN } from './features/signature';
import { SYMBOL_PLUGIN } from './features/symbol';
import { SYNTAX_HIGHLIGHTING_PLUGIN } from './features/syntax_highlighting';
import { CODE_OVERRIDES_MANAGER } from './overrides';
import { SettingsUIManager, SettingsSchemaManager } from './settings';
import {
ILSPLogConsole,
PLUGIN_ID as PLUGIN_ID_BASE,
TLanguageServerConfigurations
} from './tokens';
import { DEFAULT_TRANSCLUSIONS } from './transclusions/defaults';
import { LOG_CONSOLE } from './virtual/console';
const PLUGIN_ID = PLUGIN_ID_BASE + ':plugin';
export class LSPExtension {
get connectionManager(): ILSPDocumentConnectionManager {
return this._connectionManager;
}
private _connectionManager: DocumentConnectionManager;
languageServerManager: ILanguageServerManager;
private _settingsSchemaManager: SettingsSchemaManager;
private _isAnyActive(): boolean {
const adapters = [...this._connectionManager.adapters.values()];
return (
this.app.shell.currentWidget !== null &&
adapters.some(adapter => adapter.widget == this.app.shell.currentWidget)
);
}
constructor(
public app: JupyterFrontEnd,
private settingRegistry: ISettingRegistry,
connectionManager: DocumentConnectionManager,
public console: ILSPLogConsole,
public translator: ITranslator,
public userConsole: ILoggerRegistry | null,
statusBar: IStatusBar | null,
formRegistry: IFormRendererRegistry | null
) {
const trans = (translator || nullTranslator).load('jupyterlab_lsp');
this.languageServerManager = connectionManager.languageServerManager;
this._connectionManager = connectionManager;
const statusButtonExtension = new StatusButtonExtension({
languageServerManager: this.languageServerManager,
connectionManager: this.connectionManager,
translatorBundle: trans,
shell: app.shell
});
if (statusBar !== null) {
statusBar.registerStatusItem(PLUGIN_ID_BASE + ':language-server-status', {
item: statusButtonExtension.createItem(),
align: 'left',
rank: 1,
isActive: () => this._isAnyActive()
});
} else {
app.docRegistry.addWidgetExtension('Notebook', statusButtonExtension);
}
this._settingsSchemaManager = new SettingsSchemaManager({
settingRegistry: this.settingRegistry,
languageServerManager: this.languageServerManager,
trans: trans,
console: this.console.scope('SettingsSchemaManager'),
restored: app.restored
});
if (formRegistry != null) {
const settingsUI = new SettingsUIManager({
settingRegistry: this.settingRegistry,
console: this.console.scope('SettingsUIManager'),
languageServerManager: this.languageServerManager,
trans: trans,
schemaValidated: this._settingsSchemaManager.schemaValidated
});
// register custom UI field for `language_servers` property
formRegistry.addRenderer(`${PLUGIN_ID}.language_servers`, {
fieldRenderer: settingsUI.renderForm.bind(settingsUI)
});
}
this._settingsSchemaManager
.setupSchemaTransform(plugin.id)
.then(this._activate.bind(this))
.catch(this._activate.bind(this));
}
private _activate(): void {
this.settingRegistry
.load(plugin.id)
.then(async settings => {
await this._updateOptions(settings, false);
settings.changed.connect(async () => {
await this._updateOptions(settings, true);
});
})
.catch((reason: Error) => {
console.error(reason.message);
});
}
private async _updateOptions(
settings: ISettingRegistry.ISettings,
afterInitialization = false
) {
const options = await this._settingsSchemaManager.normalizeSettings(
settings.composite as Required<LanguageServers>
);
// Store the initial server settings, to be sent asynchronously
// when the servers are initialized.
let languageServerSettings = (options.language_servers ||
{}) as TLanguageServerConfigurations;
// Rename `serverSettings` to `configuration` to work with changed name upstream,
// rename `priority` to `rank` for the same reason.
languageServerSettings = Object.fromEntries(
Object.entries(languageServerSettings).map(([key, value]) => {
const copy = JSONExt.deepCopy(value);
copy.configuration = copy.serverSettings;
copy.rank = copy.priority;
delete copy.priority;
delete copy.serverSettings;
return [key, copy];
})
);
const previousInitialConfig = this._connectionManager.initialConfigurations;
this._connectionManager.initialConfigurations = languageServerSettings;
// TODO: if priorities changed reset connections
// update the server-independent part of configuration immediately
this.connectionManager.updateConfiguration(languageServerSettings);
if (afterInitialization) {
this.connectionManager.updateServerConfigurations(languageServerSettings);
} else {
// This would not be needed if we controlled the connection manager, but because
// it is now an independent plugin it may have started and finished initialization
// earlier, so it potentially sent wrong `initialConfigurations` and we have no way
// to avoid this, so we need to send a reconfiguration request instead.
//
// TODO: this is comparing objects which is always false,
// we should have a proper comparison to avoid redundant calls,
// but in practice because upstream never populates defaults and
// we always do, this means it would be false anyways,
// so for now the comparison serves more as a comment than logic.
if (previousInitialConfig != languageServerSettings) {
this.connectionManager.ready
.then(() => {
this.connectionManager.updateServerConfigurations(
languageServerSettings
);
})
.catch(console.error);
}
}
this._connectionManager.updateLogging(
options.logAllCommunication,
options.setTrace!
);
}
}
/**
* The plugin registration information.
*/
const plugin: JupyterFrontEndPlugin<void> = {
id: PLUGIN_ID,
requires: [
ISettingRegistry,
ILSPDocumentConnectionManager,
ILSPLogConsole,
ITranslator
],
optional: [ILoggerRegistry, IStatusBar, IFormRendererRegistry],
activate: (app, ...args) => {
new LSPExtension(
app,
...(args as [
ISettingRegistry,
DocumentConnectionManager,
ILSPLogConsole,
ITranslator,
ILoggerRegistry | null,
IStatusBar | null,
IFormRendererRegistry | null
])
);
},
autoStart: true
};
const DEFAULT_FEATURES: JupyterFrontEndPlugin<any>[] = [
JUMP_PLUGIN,
COMPLETION_PLUGIN,
SIGNATURE_PLUGIN,
HOVER_PLUGIN,
RENAME_PLUGIN,
HIGHLIGHTS_PLUGIN,
DIAGNOSTICS_PLUGIN,
SYNTAX_HIGHLIGHTING_PLUGIN,
SYMBOL_PLUGIN
];
const plugins: JupyterFrontEndPlugin<any>[] = [
LOG_CONSOLE,
COMPLETION_THEME_MANAGER,
THEME_VSCODE,
THEME_MATERIAL,
CODE_OVERRIDES_MANAGER,
NOTEBOOK_ADAPTER_PLUGIN,
FILEEDITOR_ADAPTER_PLUGIN,
plugin,
...DEFAULT_TRANSCLUSIONS,
...DEFAULT_FEATURES,
COMPLETION_FALLBACK_PLUGIN
];
/**
* Export the plugins as default.
*/
export default plugins;