model.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. IIterator,
  5. IterableOrArrayLike,
  6. iter,
  7. map,
  8. toArray
  9. } from '@phosphor/algorithm';
  10. import { JSONExt } from '@phosphor/coreutils';
  11. import { StringExt } from '@phosphor/algorithm';
  12. import { ISignal, Signal } from '@phosphor/signaling';
  13. import { Completer } from './widget';
  14. /**
  15. * An implementation of a completer model.
  16. */
  17. export class CompleterModel implements Completer.IModel {
  18. /**
  19. * A signal emitted when state of the completer menu changes.
  20. */
  21. get stateChanged(): ISignal<this, void> {
  22. return this._stateChanged;
  23. }
  24. /**
  25. * The original completion request details.
  26. */
  27. get original(): Completer.ITextState | null {
  28. return this._original;
  29. }
  30. set original(newValue: Completer.ITextState | null) {
  31. let unchanged =
  32. this._original === newValue ||
  33. (this._original &&
  34. newValue &&
  35. JSONExt.deepEqual(newValue, this._original));
  36. if (unchanged) {
  37. return;
  38. }
  39. this._reset();
  40. // Set both the current and original to the same value when original is set.
  41. this._current = this._original = newValue;
  42. this._stateChanged.emit(undefined);
  43. }
  44. /**
  45. * The current text change details.
  46. */
  47. get current(): Completer.ITextState | null {
  48. return this._current;
  49. }
  50. set current(newValue: Completer.ITextState | null) {
  51. const unchanged =
  52. this._current === newValue ||
  53. (this._current && newValue && JSONExt.deepEqual(newValue, this._current));
  54. if (unchanged) {
  55. return;
  56. }
  57. const original = this._original;
  58. // Original request must always be set before a text change. If it isn't
  59. // the model fails silently.
  60. if (!original) {
  61. return;
  62. }
  63. const cursor = this._cursor;
  64. // Cursor must always be set before a text change. This happens
  65. // automatically in the completer handler, but since `current` is a public
  66. // attribute, this defensive check is necessary.
  67. if (!cursor) {
  68. return;
  69. }
  70. const current = (this._current = newValue);
  71. if (!current) {
  72. this._stateChanged.emit(undefined);
  73. return;
  74. }
  75. const originalLine = original.text.split('\n')[original.line];
  76. const currentLine = current.text.split('\n')[current.line];
  77. // If the text change means that the original start point has been preceded,
  78. // then the completion is no longer valid and should be reset.
  79. if (currentLine.length < originalLine.length) {
  80. this.reset(true);
  81. return;
  82. }
  83. const { start, end } = this._cursor;
  84. // Clip the front of the current line.
  85. let query = current.text.substring(start);
  86. // Clip the back of the current line by calculating the end of the original.
  87. const ending = original.text.substring(end);
  88. query = query.substring(0, query.lastIndexOf(ending));
  89. this._query = query;
  90. this._stateChanged.emit(undefined);
  91. }
  92. /**
  93. * The cursor details that the API has used to return matching options.
  94. */
  95. get cursor(): Completer.ICursorSpan | null {
  96. return this._cursor;
  97. }
  98. set cursor(newValue: Completer.ICursorSpan | null) {
  99. // Original request must always be set before a cursor change. If it isn't
  100. // the model fails silently.
  101. if (!this.original) {
  102. return;
  103. }
  104. this._cursor = newValue;
  105. }
  106. /**
  107. * The query against which items are filtered.
  108. */
  109. get query(): string {
  110. return this._query;
  111. }
  112. set query(newValue: string) {
  113. this._query = newValue;
  114. }
  115. /**
  116. * A flag that is true when the model value was modified by a subset match.
  117. */
  118. get subsetMatch(): boolean {
  119. return this._subsetMatch;
  120. }
  121. set subsetMatch(newValue: boolean) {
  122. this._subsetMatch = newValue;
  123. }
  124. /**
  125. * Get whether the model is disposed.
  126. */
  127. get isDisposed(): boolean {
  128. return this._isDisposed;
  129. }
  130. /**
  131. * Dispose of the resources held by the model.
  132. */
  133. dispose(): void {
  134. // Do nothing if already disposed.
  135. if (this._isDisposed) {
  136. return;
  137. }
  138. this._isDisposed = true;
  139. Signal.clearData(this);
  140. }
  141. /**
  142. * The list of visible items in the completer menu.
  143. *
  144. * #### Notes
  145. * This is a read-only property.
  146. */
  147. items(): IIterator<Completer.IItem> {
  148. return this._filter();
  149. }
  150. /**
  151. * The unfiltered list of all available options in a completer menu.
  152. */
  153. options(): IIterator<string> {
  154. return iter(this._options);
  155. }
  156. /**
  157. * The map from identifiers (a.b) to types (function, module, class, instance,
  158. * etc.).
  159. *
  160. * #### Notes
  161. * A type map is currently only provided by the latest IPython kernel using
  162. * the completer reply metadata field `_jupyter_types_experimental`. The
  163. * values are completely up to the kernel.
  164. *
  165. */
  166. typeMap(): Completer.TypeMap {
  167. return this._typeMap;
  168. }
  169. /**
  170. * An ordered list of all the known types in the typeMap.
  171. *
  172. * #### Notes
  173. * To visually encode the types of the completer matches, we assemble an
  174. * ordered list. This list begins with:
  175. * ```
  176. * ['function', 'instance', 'class', 'module', 'keyword']
  177. * ```
  178. * and then has any remaining types listed alphebetically. This will give
  179. * reliable visual encoding for these known types, but allow kernels to
  180. * provide new types.
  181. */
  182. orderedTypes(): string[] {
  183. return this._orderedTypes;
  184. }
  185. /**
  186. * Set the available options in the completer menu.
  187. */
  188. setOptions(
  189. newValue: IterableOrArrayLike<string>,
  190. typeMap?: Completer.TypeMap
  191. ) {
  192. const values = toArray(newValue || []);
  193. const types = typeMap || {};
  194. if (
  195. JSONExt.deepEqual(values, this._options) &&
  196. JSONExt.deepEqual(types, this._typeMap)
  197. ) {
  198. return;
  199. }
  200. if (values.length) {
  201. this._options = values;
  202. this._typeMap = types;
  203. this._orderedTypes = Private.findOrderedTypes(types);
  204. } else {
  205. this._options = [];
  206. this._typeMap = {};
  207. this._orderedTypes = [];
  208. }
  209. this._stateChanged.emit(undefined);
  210. }
  211. /**
  212. * Handle a cursor change.
  213. */
  214. handleCursorChange(change: Completer.ITextState): void {
  215. // If there is no active completion, return.
  216. if (!this._original) {
  217. return;
  218. }
  219. const { column, line } = change;
  220. const { current, original } = this;
  221. if (!original) {
  222. return;
  223. }
  224. // If a cursor change results in a the cursor being on a different line
  225. // than the original request, cancel.
  226. if (line !== original.line) {
  227. this.reset(true);
  228. return;
  229. }
  230. // If a cursor change results in the cursor being set to a position that
  231. // precedes the original column, cancel.
  232. if (column < original.column) {
  233. this.reset(true);
  234. return;
  235. }
  236. const { cursor } = this;
  237. if (!cursor || !current) {
  238. return;
  239. }
  240. // If a cursor change results in the cursor being set to a position beyond
  241. // the end of the area that would be affected by completion, cancel.
  242. const cursorDelta = cursor.end - cursor.start;
  243. const originalLine = original.text.split('\n')[original.line];
  244. const currentLine = current.text.split('\n')[current.line];
  245. const inputDelta = currentLine.length - originalLine.length;
  246. if (column > original.column + cursorDelta + inputDelta) {
  247. this.reset(true);
  248. return;
  249. }
  250. }
  251. /**
  252. * Handle a text change.
  253. */
  254. handleTextChange(change: Completer.ITextState): void {
  255. const original = this._original;
  256. // If there is no active completion, return.
  257. if (!original) {
  258. return;
  259. }
  260. // When the completer detects a common subset prefix for all options,
  261. // it updates the model and sets the model source to that value, but this
  262. // text change should be ignored.
  263. if (this._subsetMatch) {
  264. return;
  265. }
  266. const { text, column, line } = change;
  267. const last = text.split('\n')[line][column - 1];
  268. // If last character entered is not whitespace or if the change column is
  269. // greater than or equal to the original column, update completion.
  270. if ((last && last.match(/\S/)) || change.column >= original.column) {
  271. this.current = change;
  272. return;
  273. }
  274. // If final character is whitespace, reset completion.
  275. this.reset(true);
  276. }
  277. /**
  278. * Create a resolved patch between the original state and a patch string.
  279. *
  280. * @param patch - The patch string to apply to the original value.
  281. *
  282. * @returns A patched text change or undefined if original value did not exist.
  283. */
  284. createPatch(patch: string): Completer.IPatch | undefined {
  285. const original = this._original;
  286. const cursor = this._cursor;
  287. if (!original || !cursor) {
  288. return undefined;
  289. }
  290. const { start, end } = cursor;
  291. const { text } = original;
  292. const prefix = text.substring(0, start);
  293. const suffix = text.substring(end);
  294. return { offset: (prefix + patch).length, text: prefix + patch + suffix };
  295. }
  296. /**
  297. * Reset the state of the model and emit a state change signal.
  298. *
  299. * @param hard - Reset even if a subset match is in progress.
  300. */
  301. reset(hard = false) {
  302. // When the completer detects a common subset prefix for all options,
  303. // it updates the model and sets the model source to that value, triggering
  304. // a reset. Unless explicitly a hard reset, this should be ignored.
  305. if (!hard && this._subsetMatch) {
  306. return;
  307. }
  308. this._reset();
  309. this._stateChanged.emit(undefined);
  310. }
  311. /**
  312. * Apply the query to the complete options list to return the matching subset.
  313. */
  314. private _filter(): IIterator<Completer.IItem> {
  315. let options = this._options || [];
  316. let query = this._query;
  317. if (!query) {
  318. return map(options, option => ({ raw: option, text: option }));
  319. }
  320. let results: Private.IMatch[] = [];
  321. for (let option of options) {
  322. let match = StringExt.matchSumOfSquares(option, query);
  323. if (match) {
  324. let marked = StringExt.highlight(option, match.indices, Private.mark);
  325. results.push({
  326. raw: option,
  327. score: match.score,
  328. text: marked.join('')
  329. });
  330. }
  331. }
  332. return map(results.sort(Private.scoreCmp), result => ({
  333. text: result.text,
  334. raw: result.raw
  335. }));
  336. }
  337. /**
  338. * Reset the state of the model.
  339. */
  340. private _reset(): void {
  341. this._current = null;
  342. this._cursor = null;
  343. this._options = [];
  344. this._original = null;
  345. this._query = '';
  346. this._subsetMatch = false;
  347. this._typeMap = {};
  348. this._orderedTypes = [];
  349. }
  350. private _current: Completer.ITextState | null = null;
  351. private _cursor: Completer.ICursorSpan | null = null;
  352. private _isDisposed = false;
  353. private _options: string[] = [];
  354. private _original: Completer.ITextState | null = null;
  355. private _query = '';
  356. private _subsetMatch = false;
  357. private _typeMap: Completer.TypeMap = {};
  358. private _orderedTypes: string[] = [];
  359. private _stateChanged = new Signal<this, void>(this);
  360. }
  361. /**
  362. * A namespace for completer model private data.
  363. */
  364. namespace Private {
  365. /**
  366. * The list of known type annotations of completer matches.
  367. */
  368. const KNOWN_TYPES = ['function', 'instance', 'class', 'module', 'keyword'];
  369. /**
  370. * The map of known type annotations of completer matches.
  371. */
  372. const KNOWN_MAP = KNOWN_TYPES.reduce(
  373. (acc, type) => {
  374. acc[type] = null;
  375. return acc;
  376. },
  377. {} as Completer.TypeMap
  378. );
  379. /**
  380. * A filtered completion menu matching result.
  381. */
  382. export interface IMatch {
  383. /**
  384. * The raw text of a completion match.
  385. */
  386. raw: string;
  387. /**
  388. * A score which indicates the strength of the match.
  389. *
  390. * A lower score is better. Zero is the best possible score.
  391. */
  392. score: number;
  393. /**
  394. * The highlighted text of a completion match.
  395. */
  396. text: string;
  397. }
  398. /**
  399. * Mark a highlighted chunk of text.
  400. */
  401. export function mark(value: string): string {
  402. return `<mark>${value}</mark>`;
  403. }
  404. /**
  405. * A sort comparison function for item match scores.
  406. *
  407. * #### Notes
  408. * This orders the items first based on score (lower is better), then
  409. * by locale order of the item text.
  410. */
  411. export function scoreCmp(a: IMatch, b: IMatch): number {
  412. let delta = a.score - b.score;
  413. if (delta !== 0) {
  414. return delta;
  415. }
  416. return a.raw.localeCompare(b.raw);
  417. }
  418. /**
  419. * Compute a reliably ordered list of types.
  420. *
  421. * #### Notes
  422. * The resulting list always begins with the known types:
  423. * ```
  424. * ['function', 'instance', 'class', 'module', 'keyword']
  425. * ```
  426. * followed by other types in alphabetical order.
  427. */
  428. export function findOrderedTypes(typeMap: Completer.TypeMap): string[] {
  429. const filtered = Object.keys(typeMap)
  430. .map(key => typeMap[key])
  431. .filter(value => value && !(value in KNOWN_MAP))
  432. .sort((a, b) => a.localeCompare(b));
  433. return KNOWN_TYPES.concat(filtered);
  434. }
  435. }