@krassowski/jupyterlab_go_to_definition
Version:
Jump to definition of a variable or function in JupyterLab
187 lines (156 loc) • 5.56 kB
text/typescript
import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
import * as nbformat from '@jupyterlab/nbformat';
import { IDocumentManager } from '@jupyterlab/docmanager';
import { CodeJumper, jumpers } from './jumper';
import { IJump, IJumpPosition } from '../jump';
import { _ensureFocus, _findCell, _findTargetCell } from '../notebook_private';
import { JumpHistory } from '../history';
import { TokenContext } from '../languages/analyzer';
import { Kernel } from '@jupyterlab/services';
import { CodeEditor } from '@jupyterlab/codeeditor';
import { ICodeCellModel } from '@jupyterlab/cells';
export class NotebookJumper extends CodeJumper {
notebook: Notebook;
widget: NotebookPanel;
constructor(
notebook_widget: NotebookPanel,
document_manager: IDocumentManager
) {
super();
this.widget = notebook_widget;
this.notebook = notebook_widget.content;
this.history = new JumpHistory(this.notebook.model.modelDB);
this.document_manager = document_manager;
}
get kernel(): Kernel.IKernelConnection {
return this.widget.context.sessionContext.session?.kernel;
}
get cwd() {
return this.widget.model.modelDB.basePath
.split('/')
.slice(0, -1)
.join('/');
}
get editors() {
return this.notebook.widgets.map(cell => cell.editor);
}
get language() {
let languageInfo = this.notebook.model.metadata.get(
'language_info'
) as nbformat.ILanguageInfoMetadata;
// TODO: consider version of the language as well
return languageInfo.name;
}
jump(position: IJumpPosition) {
let { token, index } = position;
// Prevents event propagation issues
setTimeout(() => {
this.notebook.deselectAll();
this.notebook.activeCellIndex = index;
_ensureFocus(this.notebook);
this.notebook.mode = 'edit';
// find out offset for the element
let activeEditor = this.notebook.activeCell.editor;
// place cursor in the line with the definition
let position = activeEditor.getPositionAt(token.offset);
activeEditor.setSelection({ start: position, end: position });
}, 0);
}
jump_to_definition(jump: IJump, index?: number) {
if (index === undefined) {
// Using `index = this._findCell(editor.host)` does not work,
// as the host editor has not switched to the clicked cell yet.
// The mouse event is utilized to workaround Firefox's issue.
if (jump.mouseEvent !== undefined) {
index = _findTargetCell(this.notebook, jump.mouseEvent).index;
} else {
index = _findCell(this.notebook, jump.origin);
}
}
// if the definition is in a different file:
// only support cases like:
// "from x import y" (clicking on y opens x.py)
// or
// "y.x" (clicking on y opens y.py)
let cell_of_origin_editor = this.editors[index];
let cell_of_origin_analyzer = this._getLanguageAnalyzerForCell(
cell_of_origin_editor
);
cell_of_origin_analyzer._maybe_setup_tokens();
let context = new TokenContext(
jump.token,
cell_of_origin_analyzer.tokens,
cell_of_origin_analyzer._get_token_index(jump.token)
);
let after_jump = () => {
this.history.store({ token: jump.token, index: index });
};
if (cell_of_origin_analyzer.isCrossFileReference(context)) {
this.jump_to_cross_file_reference(context, cell_of_origin_analyzer);
} else {
// try to get the location of definition from the kernel (for Python - using inspect)
// if has location:
// open tab with the file
this.inspect_and_jump(
context,
cell_of_origin_analyzer,
() => {
// TODO when reassigning objects:
// def xyz(): pass
// a = xyz
// x = a
// click on the last 'a' will lead to a jump to 'def xyz(): pass' instead to 'a = xyz'
// when using kernel for resolution. This may not be the expected behaviour!
// maybe we should first try to _findLastDefinition and only jump with kernel if none found
// (plus as an option - when user presses a different combination, it could be labeled "deep jump")
// if it fails, jump to the last definition in the current notebook:
let { token, cellIndex } = this._findLastDefinition(
jump.token,
index
);
// nothing found
if (!token) {
return;
}
this.jump({ token: token, index: cellIndex });
},
after_jump
);
}
}
jump_back() {
let previous_position = this.history.recollect();
if (previous_position) {
this.jump(previous_position);
}
}
getOffset(position: CodeEditor.IPosition, cell: number = 0) {
return this.editors[cell].getOffsetAt(position);
}
getJumpPosition(position: CodeEditor.IPosition, input_number: number) {
let cells = this.widget.model.cells.iter();
let cell = cells.next();
let i = 0;
let cell_index: number;
while (cell) {
if (cell.type === 'code') {
let code_cell = cell as ICodeCellModel;
if (code_cell.executionCount === input_number) {
cell_index = i;
break;
}
}
cell = cells.next();
i += 1;
}
// TODO: what if we cannot get the cell index?
return {
token: {
offset: this.getOffset(position, cell_index),
value: ''
},
index: cell_index
};
}
}
jumpers.set('notebook', NotebookJumper);