123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import {
- CodeEditor
- } from '@jupyterlab/codeeditor';
- import {
- IDataConnector, Text
- } from '@jupyterlab/coreutils';
- import {
- ReadonlyJSONObject, JSONObject, JSONArray
- } from '@phosphor/coreutils';
- import {
- IDisposable
- } from '@phosphor/disposable';
- import {
- Message, MessageLoop
- } from '@phosphor/messaging';
- import {
- Signal
- } from '@phosphor/signaling';
- import {
- Completer
- } from './widget';
- /**
- * A class added to editors that can host a completer.
- */
- const COMPLETER_ENABLED_CLASS: string = 'jp-mod-completer-enabled';
- /**
- * A class added to editors that have an active completer.
- */
- const COMPLETER_ACTIVE_CLASS: string = 'jp-mod-completer-active';
- /**
- * A completion handler for editors.
- */
- export
- class CompletionHandler implements IDisposable {
- /**
- * Construct a new completion handler for a widget.
- */
- constructor(options: CompletionHandler.IOptions) {
- this.completer = options.completer;
- this.completer.selected.connect(this.onCompletionSelected, this);
- this.completer.visibilityChanged.connect(this.onVisibilityChanged, this);
- this._connector = options.connector;
- }
- /**
- * The completer widget managed by the handler.
- */
- readonly completer: Completer;
- /**
- * The data connector used to populate completion requests.
- *
- * #### Notes
- * The only method of this connector that will ever be called is `fetch`, so
- * it is acceptable for the other methods to be simple functions that return
- * rejected promises.
- */
- get connector(): IDataConnector<CompletionHandler.IReply, void, CompletionHandler.IRequest> {
- return this._connector;
- }
- set connector(connector: IDataConnector<CompletionHandler.IReply, void, CompletionHandler.IRequest>) {
- this._connector = connector;
- }
- /**
- * The editor used by the completion handler.
- */
- get editor(): CodeEditor.IEditor | null {
- return this._editor;
- }
- set editor(newValue: CodeEditor.IEditor | null) {
- if (newValue === this._editor) {
- return;
- }
- let editor = this._editor;
- // Clean up and disconnect from old editor.
- if (editor && !editor.isDisposed) {
- const model = editor.model;
- editor.host.classList.remove(COMPLETER_ENABLED_CLASS);
- model.selections.changed.disconnect(this.onSelectionsChanged, this);
- model.value.changed.disconnect(this.onTextChanged, this);
- }
- // Reset completer state.
- this.completer.reset();
- this.completer.editor = newValue;
- // Update the editor and signal connections.
- editor = this._editor = newValue;
- if (editor) {
- const model = editor.model;
- this._enabled = false;
- model.selections.changed.connect(this.onSelectionsChanged, this);
- model.value.changed.connect(this.onTextChanged, this);
- // On initial load, manually check the cursor position.
- this.onSelectionsChanged();
- }
- }
- /**
- * Get whether the completion handler is disposed.
- */
- get isDisposed(): boolean {
- return this._isDisposed;
- }
- /**
- * Dispose of the resources used by the handler.
- */
- dispose(): void {
- if (this.isDisposed) {
- return;
- }
- this._isDisposed = true;
- Signal.clearData(this);
- }
- /**
- * Invoke the handler and launch a completer.
- */
- invoke(): void {
- MessageLoop.sendMessage(this, CompletionHandler.Msg.InvokeRequest);
- }
- /**
- * Process a message sent to the completion handler.
- */
- processMessage(msg: Message): void {
- switch (msg.type) {
- case CompletionHandler.Msg.InvokeRequest.type:
- this.onInvokeRequest(msg);
- break;
- default:
- break;
- }
- }
- /**
- * Get the state of the text editor at the given position.
- */
- protected getState(editor: CodeEditor.IEditor, position: CodeEditor.IPosition): Completer.ITextState {
- return {
- text: editor.model.value.text,
- lineHeight: editor.lineHeight,
- charWidth: editor.charWidth,
- line: position.line,
- column: position.column
- };
- }
- /**
- * Handle a completion selected signal from the completion widget.
- */
- protected onCompletionSelected(completer: Completer, value: string): void {
- const model = completer.model;
- const editor = this._editor;
- if (!editor || !model) {
- return;
- }
- const patch = model.createPatch(value);
- if (!patch) {
- return;
- }
- const { offset, text } = patch;
- editor.model.value.text = text;
- const position = editor.getPositionAt(offset);
- if (position) {
- editor.setCursorPosition(position);
- }
- }
- /**
- * Handle `invoke-request` messages.
- */
- protected onInvokeRequest(msg: Message): void {
- // If there is no completer model, bail.
- if (!this.completer.model) {
- return;
- }
- // If a completer session is already active, bail.
- if (this.completer.model.original) {
- return;
- }
- let editor = this._editor;
- if (editor) {
- this._makeRequest(editor.getCursorPosition())
- .catch(reason => { console.log('Invoke request bailed', reason); });
- }
- }
- /**
- * Handle selection changed signal from an editor.
- *
- * #### Notes
- * If a sub-class reimplements this method, then that class must either call
- * its super method or it must take responsibility for adding and removing
- * the completer completable class to the editor host node.
- *
- * Despite the fact that the editor widget adds a class whenever there is a
- * primary selection, this method checks independently for two reasons:
- *
- * 1. The editor widget connects to the same signal to add that class, so
- * there is no guarantee that the class will be added before this method
- * is invoked so simply checking for the CSS class's existence is not an
- * option. Secondarily, checking the editor state should be faster than
- * querying the DOM in either case.
- * 2. Because this method adds a class that indicates whether completer
- * functionality ought to be enabled, relying on the behavior of the
- * `jp-mod-has-primary-selection` to filter out any editors that have
- * a selection means the semantic meaning of `jp-mod-completer-enabled`
- * is obscured because there may be cases where the enabled class is added
- * even though the completer is not available.
- */
- protected onSelectionsChanged(): void {
- const model = this.completer.model;
- const editor = this._editor;
- if (!editor) {
- return;
- }
- const host = editor.host;
- // If there is no model, return.
- if (!model) {
- this._enabled = false;
- host.classList.remove(COMPLETER_ENABLED_CLASS);
- return;
- }
- const position = editor.getCursorPosition();
- const line = editor.getLine(position.line);
- if (!line) {
- this._enabled = false;
- model.reset(true);
- host.classList.remove(COMPLETER_ENABLED_CLASS);
- return;
- }
- const { start, end } = editor.getSelection();
- // If there is a text selection, return.
- if (start.column !== end.column || start.line !== end.line) {
- this._enabled = false;
- model.reset(true);
- host.classList.remove(COMPLETER_ENABLED_CLASS);
- return;
- }
- // If the part of the line before the cursor is white space, return.
- if (line.slice(0, position.column).match(/^\s*$/)) {
- this._enabled = false;
- model.reset(true);
- host.classList.remove(COMPLETER_ENABLED_CLASS);
- return;
- }
- // Enable completion.
- if (!this._enabled) {
- this._enabled = true;
- host.classList.add(COMPLETER_ENABLED_CLASS);
- }
- // Dispatch the cursor change.
- model.handleCursorChange(this.getState(editor, editor.getCursorPosition()));
- }
- /**
- * Handle a text changed signal from an editor.
- */
- protected onTextChanged(): void {
- const model = this.completer.model;
- if (!model || !this._enabled) {
- return;
- }
- // If there is a text selection, no completion is allowed.
- const editor = this.editor;
- if (!editor) {
- return;
- }
- const { start, end } = editor.getSelection();
- if (start.column !== end.column || start.line !== end.line) {
- return;
- }
- // Dispatch the text change.
- model.handleTextChange(this.getState(editor, editor.getCursorPosition()));
- }
- /**
- * Handle a visiblity change signal from a completer widget.
- */
- protected onVisibilityChanged(completer: Completer): void {
- // Completer is not active.
- if (completer.isDisposed || completer.isHidden) {
- if (this._editor) {
- this._editor.host.classList.remove(COMPLETER_ACTIVE_CLASS);
- this._editor.focus();
- }
- return;
- }
- // Completer is active.
- if (this._editor) {
- this._editor.host.classList.add(COMPLETER_ACTIVE_CLASS);
- }
- }
- /**
- * Make a completion request.
- */
- private _makeRequest(position: CodeEditor.IPosition): Promise<void> {
- const editor = this.editor;
- if (!editor) {
- return Promise.reject(new Error('No active editor'));
- }
- const text = editor.model.value.text;
- const offset = Text.jsIndexToCharIndex(editor.getOffsetAt(position), text);
- const pending = ++this._pending;
- const state = this.getState(editor, position);
- const request: CompletionHandler.IRequest = { text, offset };
- return this._connector.fetch(request).then(reply => {
- if (this.isDisposed) {
- throw new Error('Handler is disposed');
- }
- // If a newer completion request has created a pending request, bail.
- if (pending !== this._pending) {
- throw new Error('A newer completion request is pending');
- }
- this._onReply(state, reply);
- }).catch(reason => {
- // Completion request failures or negative results fail silently.
- const model = this.completer.model;
- if (model) {
- model.reset(true);
- }
- });
- }
- /**
- * Receive a completion reply from the connector.
- *
- * @param state - The state of the editor when completion request was made.
- *
- * @param reply - The API response returned for a completion request.
- */
- private _onReply(state: Completer.ITextState, reply: CompletionHandler.IReply): void {
- const model = this.completer.model;
- const text = state.text;
- if (!model) {
- return;
- }
- // Update the original request.
- model.original = state;
- // Dedupe the matches.
- const matches: string[] = [];
- const matchSet = new Set(reply.matches || []);
- if (reply.matches) {
- matchSet.forEach(match => { matches.push(match); });
- }
- // Extract the optional type map. The current implementation uses
- // _jupyter_types_experimental which provide string type names. We make no
- // assumptions about the names of the types, so other kernels can provide
- // their own types.
- const types = reply.metadata._jupyter_types_experimental as JSONArray;
- const typeMap: Completer.TypeMap = { };
- if (types) {
- types.forEach((item: JSONObject) => {
- // For some reason the _jupyter_types_experimental list has two entries
- // for each match, with one having a type of "<unknown>". Discard those
- // and use undefined to indicate an unknown type.
- const text = item.text as string;
- const type = item.type as string;
- if (matchSet.has(text) && type !== '<unknown>') {
- typeMap[text] = type;
- }
- });
- }
- // Update the options, including the type map.
- model.setOptions(matches, typeMap);
- // Update the cursor.
- model.cursor = {
- start: Text.charIndexToJsIndex(reply.start, text),
- end: Text.charIndexToJsIndex(reply.end, text)
- };
- }
- private _connector: IDataConnector<CompletionHandler.IReply, void, CompletionHandler.IRequest>;
- private _editor: CodeEditor.IEditor | null = null;
- private _enabled = false;
- private _pending = 0;
- private _isDisposed = false;
- }
- /**
- * A namespace for cell completion handler statics.
- */
- export
- namespace CompletionHandler {
- /**
- * The instantiation options for cell completion handlers.
- */
- export
- interface IOptions {
- /**
- * The completion widget the handler will connect to.
- */
- completer: Completer;
- /**
- * The data connector used to populate completion requests.
- *
- * #### Notes
- * The only method of this connector that will ever be called is `fetch`, so
- * it is acceptable for the other methods to be simple functions that return
- * rejected promises.
- */
- connector: IDataConnector<IReply, void, IRequest>;
- }
- /**
- * A reply to a completion request.
- */
- export
- interface IReply {
- /**
- * The starting index for the substring being replaced by completion.
- */
- start: number;
- /**
- * The end index for the substring being replaced by completion.
- */
- end: number;
- /**
- * A list of matching completion strings.
- */
- matches: string[];
- /**
- * Any metadata that accompanies the completion reply.
- */
- metadata: ReadonlyJSONObject;
- }
- /**
- * The details of a completion request.
- */
- export
- interface IRequest {
- /**
- * The cursor offset position within the text being completed.
- */
- offset: number;
- /**
- * The text being completed.
- */
- text: string;
- }
- /**
- * A namespace for completion handler messages.
- */
- export
- namespace Msg {
- /* tslint:disable */
- /**
- * A singleton `'invoke-request'` message.
- */
- export
- const InvokeRequest = new Message('invoke-request');
- /* tslint:enable */
- }
- }
|