@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
245 lines • 10.2 kB
JavaScript
import { InputDialog, Notification, ICommandPalette } from '@jupyterlab/apputils';
import { ILSPFeatureManager, ILSPDocumentConnectionManager } from '@jupyterlab/lsp';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
import { LabIcon } from '@jupyterlab/ui-components';
import renameSvg from '../../style/icons/rename.svg';
import { ContextAssembler } from '../context';
import { PositionConverter, editorAtRootPosition, rootPositionToEditorPosition } from '../converter';
import { EditApplicator } from '../edits';
import { FeatureSettings, Feature } from '../feature';
import { PLUGIN_ID } from '../tokens';
import { BrowserConsole } from '../virtual/console';
import { IDiagnosticsFeature } from './diagnostics/tokens';
export const renameIcon = new LabIcon({
name: 'lsp:rename',
svgstr: renameSvg
});
const FEATURE_ID = PLUGIN_ID + ':rename';
export class RenameFeature extends Feature {
constructor(options) {
super(options);
this.id = RenameFeature.id;
this.capabilities = {
textDocument: {
rename: {
prepareSupport: false,
honorsChangeAnnotations: false
}
}
};
this.console = new BrowserConsole().scope('Rename');
this._trans = options.trans;
}
async handleRename(workspaceEdit, oldValue, newValue, adapter, document) {
let outcome;
const applicator = new EditApplicator(document, adapter);
try {
outcome = await applicator.applyEdit(workspaceEdit);
}
catch (error) {
Notification.emit(this._trans.__('Rename failed: %1', error), 'error');
return;
}
try {
let status;
const changeText = this._trans.__('%1 to %2', oldValue, newValue);
let severity = 'success';
if (outcome.appliedChanges === 0) {
status = this._trans.__('Could not rename %1 - consult the language server documentation', changeText);
severity = 'warning';
}
else if (outcome.wasGranular) {
status = this._trans._n('Renamed %2 in %3 place', 'Renamed %2 in %3 places', outcome.appliedChanges, changeText, outcome.appliedChanges);
}
else if (adapter.hasMultipleEditors) {
status = this._trans._n('Renamed %2 in %3 cell', 'Renamed %2 in %3 cells', outcome.modifiedCells, changeText, outcome.modifiedCells);
}
else {
status = this._trans.__('Renamed %1', changeText);
}
if (outcome.errors.length !== 0) {
status += this._trans.__(' with errors: %1', outcome.errors);
severity = 'error';
}
Notification.emit(status, severity, {
autoClose: (severity === 'error' ? 5 : 3) * 1000
});
}
catch (error) {
this.console.warn(error);
}
return outcome;
}
}
/**
* In #115 an issue with rename for Python (when using pyls) was identified:
* rename was failing with an obscure message when the source code could
* not be parsed correctly by rope (due to a user's syntax error).
*
* This function detects such a condition using diagnostics feature
* and provides a nice error message to the user.
*/
function guessFailureReason(error, adapter, diagnostics, trans) {
let hasIndexError = false;
try {
hasIndexError = error.message.includes('IndexError');
}
catch (e) {
return null;
}
if (!hasIndexError) {
return null;
}
let direPythonErrors = (diagnostics.getDiagnosticsDB(adapter).all || []).filter(diagnostic => diagnostic.diagnostic.message.includes('invalid syntax') ||
diagnostic.diagnostic.message.includes('SyntaxError') ||
diagnostic.diagnostic.message.includes('IndentationError'));
if (direPythonErrors.length === 0) {
return null;
}
let direErrors = [
...new Set(direPythonErrors.map(diagnostic => {
let message = diagnostic.diagnostic.message;
let start = diagnostic.range.start;
if (adapter.hasMultipleEditors) {
let editorIndex = adapter.editors.findIndex(e => e.ceEditor === diagnostic.editorAccessor);
let cellNumber = editorIndex === -1 ? '(?)' : editorIndex + 1;
return trans.__('%1 in cell %2 at line %3', message, cellNumber, start.line);
}
else {
return trans.__('%1 at line %2', message, start.line);
}
}))
].join(', ');
return trans.__('Syntax error(s) prevents rename: %1', direErrors);
}
(function (RenameFeature) {
RenameFeature.id = FEATURE_ID;
})(RenameFeature || (RenameFeature = {}));
export var CommandIDs;
(function (CommandIDs) {
CommandIDs.renameSymbol = 'lsp:rename-symbol';
})(CommandIDs || (CommandIDs = {}));
export const RENAME_PLUGIN = {
id: FEATURE_ID,
requires: [
ILSPFeatureManager,
ISettingRegistry,
ILSPDocumentConnectionManager
],
optional: [ICommandPalette, IDiagnosticsFeature, ITranslator],
autoStart: true,
activate: async (app, featureManager, settingRegistry, connectionManager, palette, diagnostics, translator) => {
const trans = (translator || nullTranslator).load('jupyterlab_lsp');
const settings = new FeatureSettings(settingRegistry, RenameFeature.id);
await settings.ready;
if (settings.composite.disable) {
return;
}
const feature = new RenameFeature({
trans,
connectionManager
});
featureManager.register(feature);
const assembler = new ContextAssembler({
app,
connectionManager
});
app.commands.addCommand(CommandIDs.renameSymbol, {
execute: async () => {
const context = assembler.getContext();
if (!context) {
return;
}
const { adapter, connection, virtualPosition, rootPosition, document } = context;
const editorAccessor = editorAtRootPosition(adapter, rootPosition);
const editor = editorAccessor === null || editorAccessor === void 0 ? void 0 : editorAccessor.getEditor();
if (!editor) {
console.log('Could not rename - no editor');
return;
}
const editorPosition = rootPositionToEditorPosition(adapter, rootPosition);
const offset = editor.getOffsetAt(PositionConverter.cm_to_ce(editorPosition));
let oldValue = editor.getTokenAt(offset).value;
let handleFailure = (error) => {
let status = '';
if (diagnostics) {
status = guessFailureReason(error, adapter, diagnostics, trans);
}
if (!status) {
Notification.error(trans.__(`Rename failed: %1`, error), {
autoClose: 5 * 1000
});
}
else {
Notification.warning(status, { autoClose: 3 * 1000 });
}
};
const dialogValue = await InputDialog.getText({
title: trans.__('Rename to'),
text: oldValue,
okLabel: trans.__('Rename'),
cancelLabel: trans.__('Cancel')
});
try {
const newValue = dialogValue.value;
if (dialogValue.button.accept != true || newValue == null) {
// the user has cancelled the rename action or did not provide new value
return;
}
Notification.info(trans.__('Renaming %1 to %2…', oldValue, newValue), { autoClose: 3 * 1000 });
const edit = await connection.clientRequests['textDocument/rename'].request({
position: {
line: virtualPosition.line,
character: virtualPosition.ch
},
textDocument: {
uri: document.documentInfo.uri
},
newName: newValue
});
if (edit) {
await feature.handleRename(edit, oldValue, newValue, adapter, document);
}
else {
handleFailure(new Error('no edit from server'));
}
}
catch (error) {
handleFailure(error);
}
},
isVisible: () => {
const context = assembler.getContext();
if (!context) {
return false;
}
const { connection } = context;
return (connection != null &&
connection.isReady &&
connection.provides('renameProvider'));
},
isEnabled: () => {
return assembler.isContextMenuOverToken() ? true : false;
},
label: trans.__('Rename symbol'),
icon: renameIcon
});
// add to menus
app.contextMenu.addItem({
selector: '.jp-Notebook .jp-CodeCell .jp-Editor',
command: CommandIDs.renameSymbol,
rank: 10
});
app.contextMenu.addItem({
selector: '.jp-FileEditor',
command: CommandIDs.renameSymbol,
rank: 0
});
palette.addItem({
command: CommandIDs.renameSymbol,
category: trans.__('Language Server Protocol')
});
}
};
//# sourceMappingURL=rename.js.map