index.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. JSONObject
  5. } from '@phosphor/coreutils';
  6. import {
  7. JupyterLab, JupyterLabPlugin
  8. } from '@jupyterlab/application';
  9. import {
  10. ILayoutRestorer, InstanceTracker, IStateDB
  11. } from '@jupyterlab/apputils';
  12. import {
  13. IEditorServices
  14. } from '@jupyterlab/codeeditor';
  15. import {
  16. IDocumentRegistry
  17. } from '@jupyterlab/docregistry';
  18. import {
  19. IEditorTracker, FileEditor, FileEditorFactory
  20. } from '@jupyterlab/fileeditor';
  21. import {
  22. ILauncher
  23. } from '@jupyterlab/launcher';
  24. import {
  25.  MarkdownCodeBlocks
  26. } from '@jupyterlab/coreutils'
  27. /**
  28. * The class name for the text editor icon from the default theme.
  29. */
  30. const EDITOR_ICON_CLASS = 'jp-ImageTextEditor';
  31. /**
  32. * The name of the factory that creates editor widgets.
  33. */
  34. const FACTORY = 'Editor';
  35. /**
  36. * The command IDs used by the fileeditor plugin.
  37. */
  38. namespace CommandIDs {
  39. export
  40. const lineNumbers = 'editor:line-numbers';
  41. export
  42. const wordWrap = 'editor:word-wrap';
  43. export
  44. const createConsole = 'editor:create-console';
  45. export
  46. const runCode = 'editor:run-code';
  47. };
  48. /**
  49. * The editor tracker extension.
  50. */
  51. const plugin: JupyterLabPlugin<IEditorTracker> = {
  52. activate,
  53. id: 'jupyter.services.editor-tracker',
  54. requires: [IDocumentRegistry, ILayoutRestorer, IEditorServices, IStateDB],
  55. optional: [ILauncher],
  56. provides: IEditorTracker,
  57. autoStart: true
  58. };
  59. /**
  60. * Export the plugins as default.
  61. */
  62. export default plugin;
  63. /**
  64. * Activate the editor tracker plugin.
  65. */
  66. function activate(app: JupyterLab, registry: IDocumentRegistry, restorer: ILayoutRestorer, editorServices: IEditorServices, state: IStateDB, launcher: ILauncher | null): IEditorTracker {
  67. const factory = new FileEditorFactory({
  68. editorServices,
  69. factoryOptions: {
  70. name: FACTORY,
  71. fileExtensions: ['*'],
  72. defaultFor: ['*']
  73. }
  74. });
  75. const { commands, shell } = app;
  76. const id = 'editor:settings';
  77. const tracker = new InstanceTracker<FileEditor>({
  78. namespace: 'editor',
  79. shell
  80. });
  81. let lineNumbers = true;
  82. let wordWrap = true;
  83. // Handle state restoration.
  84. restorer.restore(tracker, {
  85. command: 'file-operations:open',
  86. args: widget => ({ path: widget.context.path, factory: FACTORY }),
  87. name: widget => widget.context.path
  88. });
  89. // Fetch the initial state of the settings.
  90. state.fetch(id).then(settings => {
  91. if (!settings) {
  92. return;
  93. }
  94. if (typeof settings['wordWrap'] === 'string') {
  95. commands.execute(CommandIDs.wordWrap, settings);
  96. }
  97. if (typeof settings['lineNumbers'] === 'string') {
  98. commands.execute(CommandIDs.lineNumbers, settings);
  99. }
  100. });
  101. /**
  102. * Save the editor widget settings state.
  103. */
  104. function saveState(): Promise<void> {
  105. return state.save(id, { lineNumbers, wordWrap });
  106. }
  107. factory.widgetCreated.connect((sender, widget) => {
  108. widget.title.icon = EDITOR_ICON_CLASS;
  109. // Notify the instance tracker if restore data needs to update.
  110. widget.context.pathChanged.connect(() => { tracker.save(widget); });
  111. tracker.add(widget);
  112. widget.editor.lineNumbers = lineNumbers;
  113. widget.editor.wordWrap = wordWrap;
  114. });
  115. registry.addWidgetFactory(factory);
  116. /**
  117. * Handle the settings of new widgets.
  118. */
  119. tracker.widgetAdded.connect((sender, widget) => {
  120. let editor = widget.editor;
  121. editor.lineNumbers = lineNumbers;
  122. editor.wordWrap = wordWrap;
  123. });
  124. /**
  125. * Toggle editor line numbers
  126. */
  127. function toggleLineNums(args: JSONObject): Promise<void> {
  128. lineNumbers = !lineNumbers;
  129. tracker.forEach(widget => {
  130. widget.editor.lineNumbers = lineNumbers;
  131. });
  132. return saveState();
  133. }
  134. /**
  135. * Toggle editor line wrap
  136. */
  137. function toggleLineWrap(args: JSONObject): Promise<void> {
  138. wordWrap = !wordWrap;
  139. tracker.forEach(widget => {
  140. widget.editor.wordWrap = wordWrap;
  141. });
  142. return saveState();
  143. }
  144. /**
  145. * A test for whether the tracker has an active widget.
  146. */
  147. function hasWidget(): boolean {
  148. return tracker.currentWidget !== null;
  149. }
  150. /** To detect if there is no current selection */
  151. function hasSelection(): boolean {
  152. let widget = tracker.currentWidget;
  153. if (!widget) {
  154. return false;
  155. }
  156. const editor = widget.editor;
  157. const selection = editor.getSelection();
  158. if (selection.start.column == selection.end.column &&
  159. selection.start.line == selection.end.line) {
  160. return false;
  161. }
  162. return true;
  163. }
  164. commands.addCommand(CommandIDs.lineNumbers, {
  165. execute: toggleLineNums,
  166. isEnabled: hasWidget,
  167. isToggled: () => { return lineNumbers; },
  168. label: 'Line Numbers'
  169. });
  170. commands.addCommand(CommandIDs.wordWrap, {
  171. execute: toggleLineWrap,
  172. isEnabled: hasWidget,
  173. isToggled: () => { return wordWrap; },
  174. label: 'Word Wrap'
  175. });
  176. commands.addCommand(CommandIDs.createConsole, {
  177. execute: args => {
  178. let widget = tracker.currentWidget;
  179. if (!widget) {
  180. return;
  181. }
  182. let options: JSONObject = {
  183. path: widget.context.path,
  184. preferredLanguage: widget.context.model.defaultKernelLanguage,
  185. activate: args['activate']
  186. };
  187. return commands.execute('console:create', options);
  188. },
  189. isEnabled: hasWidget,
  190. label: 'Create Console for Editor'
  191. });
  192. commands.addCommand(CommandIDs.runCode, {
  193. execute: args => {
  194. let widget = tracker.currentWidget;
  195. if (!widget) {
  196. return;
  197. }
  198. var code = ""
  199. const editor = widget.editor;
  200. const extension = widget.context.path.substring(widget.context.path.lastIndexOf("."));
  201. if (!hasSelection() && MarkdownCodeBlocks.isMarkdown(extension)) {
  202. var codeBlocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(editor.model.value.text);
  203. for (let codeBlock of codeBlocks) {
  204. if (codeBlock.startLine <= editor.getSelection().start.line &&
  205. editor.getSelection().start.line <= codeBlock.endLine) {
  206. code = codeBlock.code;
  207. break;
  208. }
  209. }
  210. } else {
  211. // Get the selected code from the editor.
  212. const selection = editor.getSelection();
  213. const start = editor.getOffsetAt(selection.start);
  214. const end = editor.getOffsetAt(selection.end);
  215. code = editor.model.value.text.substring(start, end);
  216. if (start == end) {
  217. code = editor.getLine(selection.start.line);
  218. }
  219. }
  220. const options: JSONObject = {
  221. path: widget.context.path,
  222. code: code,
  223. activate: false
  224. };
  225. // Advance cursor to the next line.
  226. const cursor = editor.getCursorPosition();
  227. if (cursor.line + 1 == editor.lineCount) {
  228. let text = editor.model.value.text;
  229. editor.model.value.text = text + '\n';
  230. }
  231. editor.setCursorPosition({ line: cursor.line + 1, column: cursor.column });
  232. return commands.execute('console:inject', options);
  233. },
  234. isEnabled: hasWidget,
  235. label: 'Run Code'
  236. });
  237. // Add a launcher item if the launcher is available.
  238. if (launcher) {
  239. launcher.add({
  240. args: { creatorName: 'Text File' },
  241. command: 'file-operations:create-from',
  242. name: 'Text Editor'
  243. });
  244. }
  245. return tracker;
  246. }