@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
195 lines • 7.87 kB
JavaScript
import { PositionConverter, documentAtRootPosition, rootPositionToVirtualPosition, editorPositionToRootPosition, editorAtRootPosition, rootPositionToEditorPosition } from './converter';
import { BrowserConsole } from './virtual/console';
// TODO: reconsider naming
export class ContextAssembler {
constructor(options) {
this.options = options;
this.console = new BrowserConsole().scope('ContexAssembler');
this._wasOverToken = false;
// no-op
}
/**
* Get context from current context menu (or fallback to document context).
*/
getContext() {
let context = null;
try {
context = this.getContextFromContextMenu();
}
catch (e) {
this.console.warn('contextMenu is attached, but could not get the context', e);
context = null;
}
if (context == null) {
try {
context = this._contextFromActiveDocument();
}
catch (e) {
if (e instanceof Error &&
e.message === 'Source line not mapped to virtual position') {
this.console.log('Could not get context from active document: it is expected when restoring workspace with open files');
}
else {
throw e;
}
}
}
return context;
}
isContextMenuOverToken() {
const context = this.getContextFromContextMenu();
if (!context) {
// annoyingly `isEnabled()` gets called again when mouse is over the menu
// which means we are no longer able to retrieve context; therefore we cache
// last value and return it if the mouse is over menu.
return this._wasOverToken;
}
const { rootPosition, adapter } = context;
const editorAccessor = editorAtRootPosition(adapter, rootPosition);
const editor = editorAccessor.getEditor();
if (!editor) {
return;
}
const editorPosition = rootPositionToEditorPosition(adapter, rootPosition);
const offset = editor.getOffsetAt(PositionConverter.cm_to_ce(editorPosition));
const token = editor.getTokenAt(offset);
const isOverToken = token.value !== '';
this._wasOverToken = isOverToken;
return isOverToken;
}
_contextFromActiveDocument() {
const adapter = [...this.options.connectionManager.adapters.values()].find(adapter => adapter.widget == this.options.app.shell.currentWidget);
if (!adapter) {
this.console.debug('No adapter');
return null;
}
const editor = adapter.activeEditor.getEditor();
if (editor === null) {
return null;
}
let ceCursor = editor.getCursorPosition();
let cmCursor = PositionConverter.ce_to_cm(ceCursor);
const virtualDocument = adapter.virtualDocument;
if (!virtualDocument) {
console.warn('Could not retrieve current context', virtualDocument);
return null;
}
const rootPosition = virtualDocument.transformFromEditorToRoot(adapter.activeEditor, cmCursor);
if (rootPosition == null) {
console.warn('Could not retrieve current context', virtualDocument);
return null;
}
return this._contextFromRoot(adapter, rootPosition);
}
_contextFromRoot(adapter, rootPosition) {
const document = documentAtRootPosition(adapter, rootPosition);
const connection = this.options.connectionManager.connections.get(document.uri);
const virtualPosition = rootPositionToVirtualPosition(adapter, rootPosition);
return {
document: document,
connection,
virtualPosition,
rootPosition,
adapter: adapter
};
}
adapterFromNode(leafNode) {
return [...this.options.connectionManager.adapters.values()].find(adapter => adapter.widget.node.contains(leafNode));
}
positionFromCoordinates(left, top, adapter, editorAccessor) {
if (!editorAccessor) {
return null;
}
const editorPosition = this.editorPositionFromCoordinates(left, top, editorAccessor);
if (!editorPosition) {
return null;
}
return editorPositionToRootPosition(adapter, editorAccessor, editorPosition);
}
editorPositionFromCoordinates(left, top, editorAccessor) {
const editor = editorAccessor.getEditor();
if (!editor) {
return null;
}
const position = editor.getPositionForCoordinate({
left: left,
right: left,
bottom: top,
top: top,
y: top,
x: left,
width: 0,
height: 0
});
if (!position) {
return null;
}
const editorPosition = {
ch: position.column,
line: position.line
};
return editorPosition;
}
/*
* Attempt to find editor from DOM and then (naively) find `Document.Editor`
* from `CodeEditor` isntances. The naive approach iterates all the editors
* (=cells) in the adapter, which is expensive and prone to fail in windowed notebooks.
*
* This may not be needed once https://github.com/jupyterlab/jupyterlab/pull/14920
* is finalised an released.
*/
editorFromNode(adapter, node) {
var _a;
const cmContent = node.closest('.cm-content');
if (!cmContent) {
return;
}
const cmView = (_a = cmContent === null || cmContent === void 0 ? void 0 : cmContent.cmView) === null || _a === void 0 ? void 0 : _a.view;
if (!cmView) {
return;
}
const editorAccessor = adapter.editors
.map(e => e.ceEditor)
.find(e => { var _a; return ((_a = e.getEditor()) === null || _a === void 0 ? void 0 : _a.editor) === cmView; });
return editorAccessor;
}
getContextFromContextMenu() {
// Note: could also try using this.app.contextMenu.menu.contentNode position.
// Note: could add a guard on this.app.contextMenu.menu.isAttached
// get the first node as it gives the most accurate approximation
const leafNode = this.options.app.contextMenuHitTest(() => true);
if (!leafNode) {
return null;
}
let { left, top } = leafNode.getBoundingClientRect();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
let event = this.options.app._contextMenuEvent;
// if possible, use more accurate position from the actual event
// (but this relies on an undocumented and unstable feature)
if (event !== undefined) {
left = event.clientX;
top = event.clientY;
event.stopPropagation();
}
const adapter = this.adapterFromNode(leafNode);
if (!adapter) {
return null;
}
const accessorFromNode = this.editorFromNode(adapter, leafNode);
if (!accessorFromNode) {
// Using `activeEditor` can lead to suprising results in notebook
// as a cell can be opened over a cell diffferent than the active one.
this.console.warn('Editor accessor not found from node, falling back to activeEditor');
}
const editorAccessor = accessorFromNode
? accessorFromNode
: adapter.activeEditor;
const rootPosition = this.positionFromCoordinates(left, top, adapter, editorAccessor);
if (!rootPosition) {
return null;
}
return this._contextFromRoot(adapter, rootPosition);
}
}
//# sourceMappingURL=context.js.map