UNPKG

@jupyter-lsp/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

245 lines 10.2 kB
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