@krassowski/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
333 lines • 14.2 kB
JavaScript
import { PositionConverter } from '../converter';
import { offset_at_position } from '../positioning';
import { DefaultMap, uris_equal } from '../utils';
function toDocumentChanges(changes) {
let documentChanges = [];
for (let uri of Object.keys(changes)) {
documentChanges.push({
textDocument: { uri },
edits: changes[uri]
});
}
return documentChanges;
}
function offset_from_lsp(position, lines) {
return offset_at_position(PositionConverter.lsp_to_ce(position), lines);
}
/**
* One feature of each type exists per VirtualDocument
* (the initialization is performed by the adapter).
*/
export class CodeMirrorIntegration {
constructor(options) {
this.feature = options.feature;
this.virtual_editor = options.virtual_editor;
this.virtual_document = options.virtual_document;
this.connection = options.connection;
this.status_message = options.status_message;
this.adapter = options.adapter;
this.console = this.adapter.console.scope(options.feature.name);
this.trans = options.trans;
this.editor_handlers = new Map();
this.connection_handlers = new Map();
this.wrapper_handlers = new Map();
this.is_registered = false;
}
get settings() {
return this.feature.settings;
}
get lab_integration() {
return this.feature.labIntegration;
}
register() {
// register editor handlers
for (let [event_name, handler] of this.editor_handlers) {
this.virtual_editor.on(event_name, handler);
}
// register connection handlers
for (let [event_name, handler] of this.connection_handlers) {
this.connection.on(event_name, handler);
}
// register editor wrapper handlers
this.wrapper = this.virtual_editor.getWrapperElement();
for (let [event_name, handler] of this.wrapper_handlers) {
this.wrapper.addEventListener(event_name, handler);
}
this.is_registered = true;
}
remove() {
// unregister editor handlers
for (let [event_name, handler] of this.editor_handlers) {
this.virtual_editor.off(event_name, handler);
}
this.editor_handlers.clear();
// unregister connection handlers
for (let [event_name, handler] of this.connection_handlers) {
this.connection.off(event_name, handler);
}
this.connection_handlers.clear();
// unregister editor wrapper handlers
for (let [event_name, handler] of this.wrapper_handlers) {
this.wrapper.removeEventListener(event_name, handler);
}
this.wrapper_handlers.clear();
}
range_to_editor_range(range, cm_editor) {
let start = PositionConverter.lsp_to_cm(range.start);
let end = PositionConverter.lsp_to_cm(range.end);
if (cm_editor == null) {
let start_in_root = this.transform_virtual_position_to_root_position(start);
let ce_editor = this.virtual_editor.get_editor_at_root_position(start_in_root);
cm_editor = this.virtual_editor.ce_editor_to_cm_editor.get(ce_editor);
}
return {
start: this.virtual_document.transform_virtual_to_editor(start),
end: this.virtual_document.transform_virtual_to_editor(end),
editor: cm_editor
};
}
position_from_mouse(event) {
const position = this.virtual_editor.coordsChar({
left: event.clientX,
top: event.clientY
}, 'window');
if (position.line === -1 && position.ch === -1) {
return null;
}
else {
return position;
}
}
transform_virtual_position_to_root_position(start) {
let ce_editor = this.virtual_document.virtual_lines.get(start.line).editor;
let editor_position = this.virtual_document.transform_virtual_to_editor(start);
return this.virtual_editor.transform_from_editor_to_root(ce_editor, editor_position);
}
get_cm_editor(position) {
return this.virtual_editor.get_cm_editor(position);
}
get_language_at(position, editor) {
return editor.getModeAt(position).name;
}
extract_last_character(change) {
if (change.origin === 'paste') {
let last_line = change.text[change.text.length - 1];
return last_line[last_line.length - 1];
}
else {
return change.text[0][0];
}
}
highlight_range(range, class_name) {
return range.editor
.getDoc()
.markText(range.start, range.end, { className: class_name });
}
/**
* Does the edit cover the entire document?
*/
is_whole_document_edit(edit) {
let value = this.virtual_document.value;
let lines = value.split('\n');
let range = edit.range;
let lsp_to_ce = PositionConverter.lsp_to_ce;
return (offset_at_position(lsp_to_ce(range.start), lines) === 0 &&
offset_at_position(lsp_to_ce(range.end), lines) === value.length);
}
async apply_edit(workspaceEdit) {
let current_uri = this.virtual_document.document_info.uri;
// Specs: documentChanges are preferred over changes
let changes = workspaceEdit.documentChanges
? workspaceEdit.documentChanges.map(change => change)
: toDocumentChanges(workspaceEdit.changes);
let applied_changes = 0;
let edited_cells = 0;
let is_whole_document_edit = false;
let errors = [];
for (let change of changes) {
let uri = change.textDocument.uri;
if (!uris_equal(uri, current_uri)) {
errors.push(`Workspace-wide edits not implemented: ${uri} != ${current_uri}`);
}
else {
is_whole_document_edit =
change.edits.length === 1 &&
this.is_whole_document_edit(change.edits[0]);
let edit;
if (!is_whole_document_edit) {
applied_changes = 0;
let value = this.virtual_document.value;
// TODO: make sure that it was not changed since the request was sent (using the returned document version)
let lines = value.split('\n');
let edits_by_offset = new Map();
for (let e of change.edits) {
let offset = offset_from_lsp(e.range.start, lines);
if (edits_by_offset.has(offset)) {
console.warn('Edits should not overlap, ignoring an overlapping edit');
}
else {
edits_by_offset.set(offset, e);
applied_changes += 1;
}
}
// TODO make use of old_to_new_line for edits which add of remove lines:
// this is crucial to preserve cell boundaries in notebook in such cases
let old_to_new_line = new DefaultMap(() => []);
let new_text = '';
let last_end = 0;
let current_old_line = 0;
let current_new_line = 0;
// going over the edits in descending order of start points:
let start_offsets = [...edits_by_offset.keys()].sort((a, b) => a - b);
for (let start of start_offsets) {
let edit = edits_by_offset.get(start);
let prefix = value.slice(last_end, start);
for (let i = 0; i < prefix.split('\n').length; i++) {
let new_lines = old_to_new_line.get_or_create(current_old_line);
new_lines.push(current_new_line);
current_old_line += 1;
current_new_line += 1;
}
new_text += prefix + edit.newText;
let end = offset_from_lsp(edit.range.end, lines);
let replaced_fragment = value.slice(start, end);
for (let i = 0; i < edit.newText.split('\n').length; i++) {
if (i < replaced_fragment.length) {
current_old_line += 1;
}
current_new_line += 1;
let new_lines = old_to_new_line.get_or_create(current_old_line);
new_lines.push(current_new_line);
}
last_end = end;
}
new_text += value.slice(last_end, value.length);
edit = {
range: {
start: { line: 0, character: 0 },
end: {
line: lines.length - 1,
character: lines[lines.length - 1].length
}
},
newText: new_text
};
console.assert(this.is_whole_document_edit(edit));
}
else {
edit = change.edits[0];
applied_changes += 1;
}
edited_cells = this.apply_single_edit(edit);
}
}
const all_empty = changes.every(change => change.edits.length === 0);
return {
appliedChanges: applied_changes,
modifiedCells: edited_cells,
wasGranular: !is_whole_document_edit && !all_empty,
errors: errors
};
}
replace_fragment(newText, editor, fragment_start, fragment_end, start, end, is_whole_document_edit = false) {
let document = this.virtual_document;
let newFragmentText = newText
.split('\n')
.slice(fragment_start.line - start.line, fragment_end.line - start.line)
.join('\n');
if (newFragmentText.endsWith('\n')) {
newFragmentText = newFragmentText.slice(0, -1);
}
let doc = this.virtual_editor.ce_editor_to_cm_editor.get(editor).getDoc();
let raw_value = doc.getValue('\n');
// extract foreign documents and substitute magics,
// as it was done when the shadow virtual document was being created
let { lines } = document.prepare_code_block({
value: raw_value,
ce_editor: editor
});
let old_value = lines.join('\n');
if (is_whole_document_edit) {
// partial edit
let cm_to_ce = PositionConverter.cm_to_ce;
let up_to_offset = offset_at_position(cm_to_ce(start), lines);
let from_offset = offset_at_position(cm_to_ce(end), lines);
newFragmentText =
old_value.slice(0, up_to_offset) +
newText +
old_value.slice(from_offset);
}
if (old_value === newFragmentText) {
return 0;
}
let new_value = document.decode_code_block(newFragmentText);
let cursor = doc.getCursor();
doc.replaceRange(new_value, { line: 0, ch: 0 }, {
line: fragment_end.line - fragment_start.line + 1,
ch: 0
});
try {
// try to restore the cursor to the position prior to the edit
// (this might not be the best UX, but definitely better than
// the cursor jumping to the very end of the cell/file).
doc.setCursor(cursor, cursor.ch, { scroll: false });
// note: this does not matter for actions invoke from context menu
// as those loose focus anyways (this might be addressed elsewhere)
}
catch (e) {
console.log('Could not place the cursor back', e);
}
return 1;
}
afterChange(change, root_position) {
// nothing here
}
apply_single_edit(edit) {
let document = this.virtual_document;
let applied_changes = 0;
let start = PositionConverter.lsp_to_cm(edit.range.start);
let end = PositionConverter.lsp_to_cm(edit.range.end);
let start_editor = document.get_editor_at_virtual_line(start);
let end_editor = document.get_editor_at_virtual_line(end);
if (start_editor !== end_editor) {
let last_editor = start_editor;
let fragment_start = start;
let fragment_end = Object.assign({}, start);
let line = start.line;
let recently_replaced = false;
while (line <= end.line) {
line++;
let editor = document.get_editor_at_virtual_line({
line: line,
ch: 0
});
if (editor === last_editor) {
fragment_end.line = line;
fragment_end.ch = 0;
recently_replaced = false;
}
else {
applied_changes += this.replace_fragment(edit.newText, last_editor, fragment_start, fragment_end, start, end);
recently_replaced = true;
fragment_start = {
line: line,
ch: 0
};
fragment_end = {
line: line,
ch: 0
};
last_editor = editor;
}
}
if (!recently_replaced) {
applied_changes += this.replace_fragment(edit.newText, last_editor, fragment_start, fragment_end, start, end);
}
}
else {
applied_changes += this.replace_fragment(edit.newText, start_editor, start, end, start, end);
}
return applied_changes;
}
}
//# sourceMappingURL=codemirror.js.map