@krassowski/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
261 lines • 11.5 kB
JavaScript
import { CodeMirrorEditor } from '@jupyterlab/codemirror';
import { URLExt } from '@jupyterlab/coreutils';
import { IDocumentManager } from '@jupyterlab/docmanager';
import { IEditorTracker } from '@jupyterlab/fileeditor';
import { INotebookTracker } from '@jupyterlab/notebook';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { ITranslator } from '@jupyterlab/translation';
import { LabIcon } from '@jupyterlab/ui-components';
import { FileEditorJumper, NotebookJumper } from '@krassowski/code-jumpers';
import jumpToSvg from '../../style/icons/jump-to.svg';
import { PositionConverter } from '../converter';
import { CodeMirrorIntegration } from '../editor_integration/codemirror';
import { FeatureSettings } from '../feature';
import { ILSPAdapterManager, ILSPFeatureManager, PLUGIN_ID } from '../tokens';
import { getModifierState, uri_to_contents_path, uris_equal } from '../utils';
export const jumpToIcon = new LabIcon({
name: 'lsp:jump-to',
svgstr: jumpToSvg
});
const jumpBackIcon = new LabIcon({
name: 'lsp:jump-back',
svgstr: jumpToSvg.replace('jp-icon3', 'lsp-icon-flip-x jp-icon3')
});
const FEATURE_ID = PLUGIN_ID + ':jump_to';
let trans;
export class CMJumpToDefinition extends CodeMirrorIntegration {
get jumper() {
return this.feature.labIntegration.jumper;
}
get settings() {
return super.settings;
}
get modifierKey() {
return this.settings.composite.modifierKey;
}
register() {
this.editor_handlers.set('mousedown', (virtual_editor, event) => {
const { button } = event;
if (button === 0 && getModifierState(event, this.modifierKey)) {
let root_position = this.position_from_mouse(event);
if (root_position == null) {
this.console.warn('Could not retrieve root position from mouse event to jump to definition');
return;
}
let document = virtual_editor.document_at_root_position(root_position);
let virtual_position = virtual_editor.root_position_to_virtual_position(root_position);
this.connection
.getDefinition(virtual_position, document.document_info, false)
.then(targets => {
this.handle_jump(targets, document.document_info.uri).catch(this.console.warn);
})
.catch(this.console.warn);
event.preventDefault();
event.stopPropagation();
}
});
super.register();
}
get_uri_and_range(location_or_locations) {
if (location_or_locations == null) {
return undefined;
}
// some language servers appear to return a single object
const locations = Array.isArray(location_or_locations)
? location_or_locations
: [location_or_locations];
// TODO: implement selector for multiple locations
// (like when there are multiple definitions or usages)
// could use the showHints() or completion frontend as a reference
if (locations.length === 0) {
return undefined;
}
this.console.log('Will jump to the first of suggested locations:', locations);
const location_or_link = locations[0];
if ('targetUri' in location_or_link) {
return {
uri: location_or_link.targetUri,
range: location_or_link.targetRange
};
}
else if ('uri' in location_or_link) {
return {
uri: location_or_link.uri,
range: location_or_link.range
};
}
else {
this.console.warn('Returned jump location is incorrect (no uri or targetUri):', location_or_link);
return undefined;
}
}
async handle_jump(location_or_locations, document_uri) {
const target_info = this.get_uri_and_range(location_or_locations);
if (!target_info) {
this.status_message.set(trans.__('No jump targets found'), 2 * 1000);
return;
}
let { uri, range } = target_info;
let virtual_position = PositionConverter.lsp_to_cm(range.start);
if (uris_equal(uri, document_uri)) {
let editor_index = this.adapter.get_editor_index_at(virtual_position);
// if in current file, transform from the position within virtual document to the editor position:
let editor_position = this.virtual_editor.transform_virtual_to_editor(virtual_position);
if (editor_position === null) {
this.console.warn('Could not jump: conversion from virtual position to editor position failed', virtual_position);
return;
}
let editor_position_ce = PositionConverter.cm_to_ce(editor_position);
this.console.log(`Jumping to ${editor_index}th editor of ${uri}`);
this.console.log('Jump target within editor:', editor_position_ce);
let contents_path = this.adapter.widget.context.path;
this.jumper.global_jump({
line: editor_position_ce.line,
column: editor_position.ch,
editor_index: editor_index,
is_symlink: false,
contents_path: contents_path
});
}
else {
// otherwise there is no virtual document and we expect the returned position to be source position:
let source_position_ce = PositionConverter.cm_to_ce(virtual_position);
this.console.log(`Jumping to external file: ${uri}`);
this.console.log('Jump target (source location):', source_position_ce);
let jump_data = {
editor_index: 0,
line: source_position_ce.line,
column: source_position_ce.column
};
// assume that we got a relative path to a file within the project
// TODO use is_relative() or something? It would need to be not only compatible
// with different OSes but also with JupyterHub and other platforms.
// can it be resolved vs our guessed server root?
let contents_path = uri_to_contents_path(uri);
if (contents_path == null) {
if (uri.startsWith('file://')) {
contents_path = decodeURIComponent(uri.slice(7));
}
else {
contents_path = decodeURIComponent(uri);
}
}
if (contents_path === null) {
this.console.warn('contents_path could not be resolved');
return;
}
try {
await this.jumper.document_manager.services.contents.get(contents_path, {
content: false
});
this.jumper.global_jump(Object.assign(Object.assign({ contents_path }, jump_data), { is_symlink: false }));
return;
}
catch (err) {
this.console.warn(err);
}
this.jumper.global_jump(Object.assign(Object.assign({ contents_path: URLExt.join('.lsp_symlink', contents_path) }, jump_data), { is_symlink: true }));
}
}
}
class JumperLabIntegration {
constructor(settings, adapterManager, notebookTracker, documentManager, fileEditorTracker) {
this.settings = settings;
this.adapterManager = adapterManager;
this.jumpers = new Map();
if (fileEditorTracker !== null) {
fileEditorTracker.widgetAdded.connect((sender, widget) => {
let fileEditor = widget.content;
if (fileEditor.editor instanceof CodeMirrorEditor) {
let jumper = new FileEditorJumper(widget, documentManager);
this.jumpers.set(widget.id, jumper);
}
});
}
notebookTracker.widgetAdded.connect(async (sender, widget) => {
// NOTE: assuming that the default cells content factory produces CodeMirror editors(!)
let jumper = new NotebookJumper(widget, documentManager);
this.jumpers.set(widget.id, jumper);
});
}
get jumper() {
let current = this.adapterManager.currentAdapter.widget.id;
return this.jumpers.get(current);
}
}
const COMMANDS = (trans) => [
{
id: 'jump-to-definition',
execute: async ({ connection, virtual_position, document, features }) => {
const jump_feature = features.get(FEATURE_ID);
const targets = await (connection === null || connection === void 0 ? void 0 : connection.getDefinition(virtual_position, document.document_info, false));
await jump_feature.handle_jump(targets, document.document_info.uri);
},
is_enabled: ({ connection }) => connection ? connection.isDefinitionSupported() : false,
label: trans.__('Jump to definition'),
icon: jumpToIcon
},
{
id: 'jump-back',
execute: async ({ connection, virtual_position, document, features }) => {
const jump_feature = features.get(FEATURE_ID);
jump_feature.jumper.global_jump_back();
},
is_enabled: ({ connection }) => connection ? connection.isDefinitionSupported() : false,
label: trans.__('Jump back'),
icon: jumpBackIcon,
// do not attach to any of the context menus
attach_to: new Set()
}
];
export const JUMP_PLUGIN = {
id: FEATURE_ID,
requires: [
ILSPFeatureManager,
ISettingRegistry,
ILSPAdapterManager,
INotebookTracker,
IDocumentManager,
ITranslator
],
optional: [IEditorTracker],
autoStart: true,
activate: (app, featureManager, settingRegistry, adapterManager, notebookTracker, documentManager, translator, fileEditorTracker) => {
const settings = new FeatureSettings(settingRegistry, FEATURE_ID);
trans = translator.load('jupyterlab-lsp');
let labIntegration = new JumperLabIntegration(settings, adapterManager, notebookTracker, documentManager, fileEditorTracker);
featureManager.register({
feature: {
editorIntegrationFactory: new Map([
['CodeMirrorEditor', CMJumpToDefinition]
]),
commands: COMMANDS(trans),
id: FEATURE_ID,
name: 'Jump to definition',
labIntegration: labIntegration,
settings: settings,
capabilities: {
textDocument: {
declaration: {
dynamicRegistration: true,
linkSupport: true
},
definition: {
dynamicRegistration: true,
linkSupport: true
},
typeDefinition: {
dynamicRegistration: true,
linkSupport: true
},
implementation: {
dynamicRegistration: true,
linkSupport: true
}
}
}
}
});
}
};
//# sourceMappingURL=jump_to.js.map