labnotes 0.0.98__tar.gz → 0.0.99__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. {labnotes-0.0.98 → labnotes-0.0.99}/PKG-INFO +1 -1
  2. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/labnotes.css +49 -17
  3. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/new-note.js +0 -2
  4. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/note-edit.js +9 -16
  5. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/note-editor.js +113 -69
  6. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/templates/editor_imports.html +1 -0
  7. {labnotes-0.0.98 → labnotes-0.0.99}/.gitignore +0 -0
  8. {labnotes-0.0.98 → labnotes-0.0.99}/CHANGELOG.md +0 -0
  9. {labnotes-0.0.98 → labnotes-0.0.99}/MIGRATIONS.md +0 -0
  10. {labnotes-0.0.98 → labnotes-0.0.99}/README.md +0 -0
  11. {labnotes-0.0.98 → labnotes-0.0.99}/pyproject.toml +0 -0
  12. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/__init__.py +0 -0
  13. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/__main__.py +0 -0
  14. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/asset_url.py +0 -0
  15. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/check_counts.py +0 -0
  16. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/cli.py +0 -0
  17. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/context.md +0 -0
  18. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/context_guide.py +0 -0
  19. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/count_groups.py +0 -0
  20. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/count_runs.py +0 -0
  21. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/create_app.py +0 -0
  22. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/create_experiment.py +0 -0
  23. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/create_note.py +0 -0
  24. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/__init__.py +0 -0
  25. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/control_run.py +0 -0
  26. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/daemon_app.py +0 -0
  27. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/daemon_client.py +0 -0
  28. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/daemon_running.py +0 -0
  29. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/daemon_server.py +0 -0
  30. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/describe_status.py +0 -0
  31. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/ensure_daemon.py +0 -0
  32. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/exit_when_idle.py +0 -0
  33. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/remote_start.py +0 -0
  34. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/run_daemon.py +0 -0
  35. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/run_logs.py +0 -0
  36. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/serve_daemon.py +0 -0
  37. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/socket_path.py +0 -0
  38. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/stop_runs.py +0 -0
  39. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/watch_daemon.py +0 -0
  40. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon/watch_status.py +0 -0
  41. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/daemon_command.py +0 -0
  42. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/data_html.py +0 -0
  43. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/default_config_text.py +0 -0
  44. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/default_counts.py +0 -0
  45. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/drain_watch.py +0 -0
  46. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/experiment_notes.py +0 -0
  47. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/experiment_runs.py +0 -0
  48. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/experiments.py +0 -0
  49. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/file_changes.py +0 -0
  50. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/format_duration.py +0 -0
  51. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/format_started.py +0 -0
  52. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/format_value.py +0 -0
  53. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/group_runs.py +0 -0
  54. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/init.py +0 -0
  55. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/invocation_started.py +0 -0
  56. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/is_positive_int.py +0 -0
  57. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/list_experiments.py +0 -0
  58. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/list_runs.py +0 -0
  59. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/load_config.py +0 -0
  60. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/load_experiment.py +0 -0
  61. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/load_run.py +0 -0
  62. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/log.py +0 -0
  63. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/log_path.py +0 -0
  64. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/models.py +0 -0
  65. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/modes.py +0 -0
  66. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/nests_runs.py +0 -0
  67. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/note_path.py +0 -0
  68. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/notebook_kernel.py +0 -0
  69. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/output_html.py +0 -0
  70. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/parse_run_view.py +0 -0
  71. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/print_rows.py +0 -0
  72. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/python_blocks.py +0 -0
  73. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/read_config.py +0 -0
  74. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/read_path.py +0 -0
  75. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/render_markdown.py +0 -0
  76. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/run_command.py +0 -0
  77. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/run_control.py +0 -0
  78. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/run_output.py +0 -0
  79. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/run_status.py +0 -0
  80. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/__init__.py +0 -0
  81. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/describe_event.py +0 -0
  82. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/execute_run.py +0 -0
  83. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/hold_folder.py +0 -0
  84. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/new_param_run_id.py +0 -0
  85. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/new_run_id.py +0 -0
  86. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  87. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/param_args.py +0 -0
  88. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/param_grid.py +0 -0
  89. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/param_run_name.py +0 -0
  90. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/plan_runs.py +0 -0
  91. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/prepare_experiment.py +0 -0
  92. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/read_experiment_config.py +0 -0
  93. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/run_experiment.py +0 -0
  94. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/run_folder.py +0 -0
  95. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/run_headless.py +0 -0
  96. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/runs_parent.py +0 -0
  97. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/shell_command.py +0 -0
  98. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/signal_tree.py +0 -0
  99. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/stop_then_kill.py +0 -0
  100. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/wait_for_tree.py +0 -0
  101. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/runner/write_run_json.py +0 -0
  102. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/save_conflict_copy.py +0 -0
  103. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/save_note.py +0 -0
  104. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/saved_outputs.py +0 -0
  105. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/serve_app.py +0 -0
  106. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/server.py +0 -0
  107. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/shared_params.py +0 -0
  108. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/sort_href.py +0 -0
  109. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/sql_quote.py +0 -0
  110. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/elapsed.js +0 -0
  111. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/experiment.js +0 -0
  112. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/local-time.js +0 -0
  113. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/row-link.js +0 -0
  114. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/run-cell.js +0 -0
  115. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/run-output.js +0 -0
  116. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/static/view-toggle.js +0 -0
  117. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/summarize_config.py +0 -0
  118. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/__init__.py +0 -0
  119. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/experiment_rows.py +0 -0
  120. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/experiment_run_ids.py +0 -0
  121. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/file_prefix.py +0 -0
  122. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/file_text.py +0 -0
  123. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/iso_time.py +0 -0
  124. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/list_experiment_runs.py +0 -0
  125. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/note_text.py +0 -0
  126. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/open_tables.py +0 -0
  127. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/path_glob.py +0 -0
  128. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/path_pattern.py +0 -0
  129. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/read_experiment.py +0 -0
  130. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/read_file.py +0 -0
  131. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/read_log.py +0 -0
  132. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/read_run.py +0 -0
  133. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tables/summarize_experiments.py +0 -0
  134. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/templates/base.html +0 -0
  135. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/templates/experiment.html +0 -0
  136. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/templates/index.html +0 -0
  137. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/templates/note.html +0 -0
  138. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/templates/run.html +0 -0
  139. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/__init__.py +0 -0
  140. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/browse_app.py +0 -0
  141. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/browse_tui.py +0 -0
  142. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/dim.py +0 -0
  143. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/experiment_screen.py +0 -0
  144. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/experiments_screen.py +0 -0
  145. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/fill_table.py +0 -0
  146. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/format_elapsed.py +0 -0
  147. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/group_cells.py +0 -0
  148. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/invocation_columns.py +0 -0
  149. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/invocation_rows.py +0 -0
  150. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/leave_prompt.py +0 -0
  151. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/reload_on_change.py +0 -0
  152. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/run_app.py +0 -0
  153. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/run_cells.py +0 -0
  154. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/run_output.py +0 -0
  155. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/run_screen.py +0 -0
  156. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/run_tui.py +0 -0
  157. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/runs_table.py +0 -0
  158. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/tui/stopped_title.py +0 -0
  159. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/wait_for_note.py +0 -0
  160. {labnotes-0.0.98 → labnotes-0.0.99}/src/labnotes/watch_rows.py +0 -0
  161. {labnotes-0.0.98 → labnotes-0.0.99}/testing-conventions.toml +0 -0
  162. {labnotes-0.0.98 → labnotes-0.0.99}/tests/integration/conftest.py +0 -0
  163. {labnotes-0.0.98 → labnotes-0.0.99}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.98
3
+ Version: 0.0.99
4
4
  Summary: Browse experiment designs, notes, and run data from a folder.
5
5
  Project-URL: Repository, https://github.com/thekevinscott/labnotes
6
6
  Author-email: Kevin Scott <me@thekevinscott.com>
@@ -418,7 +418,7 @@ h3 {
418
418
  top: 0.5rem;
419
419
  right: 0.5rem;
420
420
  }
421
- .cell .run, .note-name .restart {
421
+ .cell .run, .editor .run, .note-name .restart {
422
422
  font: inherit;
423
423
  font-family: var(--sans);
424
424
  font-size: 0.75rem;
@@ -429,8 +429,8 @@ h3 {
429
429
  color: var(--ink-2);
430
430
  cursor: pointer;
431
431
  }
432
- .cell .run:hover, .note-name .restart:hover { color: var(--accent); border-color: var(--accent); }
433
- .cell .run:disabled, .note-name .restart:disabled { cursor: progress; opacity: 0.6; }
432
+ .cell .run:hover, .editor .run:hover, .note-name .restart:hover { color: var(--accent); border-color: var(--accent); }
433
+ .cell .run:disabled, .editor .run:disabled, .note-name .restart:disabled { cursor: progress; opacity: 0.6; }
434
434
  .note-name:has(.restart) { display: flex; align-items: center; justify-content: flex-end; padding-top: 0.45rem; padding-bottom: 0.45rem; }
435
435
  .cell-output { display: block; }
436
436
  .cell-output:not(:empty) { margin-top: 0.4rem; padding-left: 0.9rem; border-left: 2px solid var(--accent-soft); overflow-x: auto; }
@@ -438,25 +438,57 @@ h3 {
438
438
  .cell-output .stderr, .cell-output .error { color: var(--bad); }
439
439
  .cell-output img, .cell-output svg { max-width: 100%; height: auto; }
440
440
 
441
- /* Editing a note in place */
442
- .prose.editable { cursor: text; }
441
+ /* Editing a note in place: CodeMirror styled to match .prose */
443
442
  .edit-status { margin: 0; padding: 0.45rem 1.75rem; font-family: var(--sans); font-size: 0.8rem; color: var(--ink-3); border-bottom: 1px solid var(--rule); }
444
443
  .editor { padding: 0.5rem 1.75rem 1.5rem; max-width: 46rem; }
445
444
  .editor .cm-editor.cm-focused { outline: none; }
446
- .editor .cm-content { font-family: var(--serif); font-size: 1.06rem; line-height: 1.68; caret-color: var(--ink); }
445
+ .editor .cm-content { padding: 0.8em 0; font-family: var(--serif); font-size: 1.06rem; line-height: 1.68; caret-color: var(--ink); }
447
446
  .editor .cm-line { padding: 0; }
448
- .editor .md-h1 { font-size: 1.6rem; font-weight: 600; }
449
- .editor .md-h2 { font-size: 1.3rem; font-weight: 600; }
450
- .editor .md-h3 { font-size: 1.1rem; font-weight: 600; }
451
- .editor .md-h4 { font-weight: 600; }
452
- .editor .md-strong { font-weight: 700; }
453
- .editor .md-em { font-style: italic; }
454
- .editor .md-strike { text-decoration: line-through; }
455
- .editor .md-link { color: var(--accent); }
456
- .editor .md-code { font-family: var(--mono); font-size: 0.86em; }
457
- .editor .md-quote { color: var(--ink-2); }
447
+ .editor .cm-line:has(> br:only-child):not(.cm-atomic-fenced-code) { line-height: 0.8em; }
448
+ .editor .cm-atomic-h1, .editor .cm-atomic-h2, .editor .cm-atomic-h3, .editor .cm-atomic-h4, .editor .cm-atomic-h5, .editor .cm-atomic-h6 { font-weight: 600; letter-spacing: -0.01em; line-height: 1.3; padding: 1em 0 0.5em; }
449
+ .editor .cm-line.cm-atomic-h1 { font-size: 1.6rem; }
450
+ .editor .cm-line.cm-atomic-h2 { font-size: 1.3rem; }
451
+ .editor .cm-line.cm-atomic-h3 { font-size: 1.1rem; }
452
+ .editor .cm-line.cm-atomic-h4, .editor .cm-line.cm-atomic-h5, .editor .cm-line.cm-atomic-h6 { font-size: 1rem; }
453
+ .editor .cm-atomic-strong { font-weight: 700; }
454
+ .editor .cm-atomic-em { font-style: italic; }
455
+ .editor .cm-atomic-strike { text-decoration: line-through; }
456
+ .editor .cm-atomic-inline-code { font-family: var(--mono); font-size: 0.86em; background: var(--chip); padding: 0.1em 0.35em; border-radius: 4px; }
457
+ .editor .cm-atomic-link { text-decoration: underline; text-decoration-color: var(--rule-strong); text-underline-offset: 3px; }
458
+ .editor .cm-atomic-link::after {
459
+ content: "↗";
460
+ margin-left: 0.15em;
461
+ font-family: var(--sans);
462
+ font-size: 0.75em;
463
+ color: var(--ink-3);
464
+ cursor: pointer;
465
+ }
466
+ .editor .cm-atomic-link:hover::after { color: var(--accent); }
467
+ .editor .cm-line.cm-atomic-blockquote { padding-left: 1rem; border-left: 2px solid var(--rule-strong); color: var(--ink-2); }
468
+ .editor .cm-atomic-list-marker { display: inline-block; width: 0.9em; margin-right: 0.3em; text-align: right; }
469
+ .editor .cm-atomic-list-marker, .editor .cm-atomic-list-marker .md-mark { color: var(--ink); }
470
+ .editor .cm-line.cm-atomic-hr { position: relative; }
471
+ .editor .cm-line.cm-atomic-hr::after { content: ""; position: absolute; left: 0; right: 0; top: 50%; border-top: 1px solid var(--rule); }
472
+ .editor .cm-line.cm-atomic-fenced-code {
473
+ position: relative;
474
+ font-family: var(--mono);
475
+ font-size: 0.8rem;
476
+ line-height: 1.5;
477
+ background: var(--paper);
478
+ padding: 0 1rem;
479
+ border: solid var(--rule);
480
+ border-width: 0 1px;
481
+ }
482
+ .editor .cm-line:not(.cm-atomic-fenced-code) + .cm-atomic-fenced-code, .editor .cm-atomic-fenced-code:first-child { border-top-width: 1px; border-radius: 6px 6px 0 0; }
483
+ .editor .cm-atomic-fenced-code:not(:has(+ .cm-atomic-fenced-code)) { border-bottom-width: 1px; border-radius: 0 0 6px 6px; }
484
+ .editor .cm-line:not(.cm-atomic-fenced-code) + .cm-atomic-fenced-code:not(:has(+ .cm-atomic-fenced-code)) { border-radius: 6px; }
485
+ .editor .cm-atomic-fenced-selection { background: var(--accent-soft); }
486
+ .editor .run { position: absolute; z-index: 1; top: 0.5rem; right: 0.5rem; }
487
+ /* CodeMirror measures block widgets without their margins. */
488
+ .editor .cell-below { display: flow-root; padding-bottom: 0.8em; }
489
+ .editor .cell-output pre { margin: 0; font-size: 0.8rem; line-height: 1.5; }
490
+ .editor .cm-line .cm-widgetBuffer { vertical-align: top; }
458
491
  .editor .md-mark { color: var(--ink-3); }
459
- .editor .md-code-line { font-family: var(--mono); font-size: 0.8rem; line-height: 1.5; background: var(--paper); padding: 0 1rem; }
460
492
  .editor .py-keyword { color: var(--accent); }
461
493
  .editor .py-string { color: var(--good); }
462
494
  .editor .py-comment { color: var(--ink-3); font-style: italic; }
@@ -22,8 +22,6 @@ function show(note, button) {
22
22
  }
23
23
 
24
24
  for (const button of document.querySelectorAll(".new-note")) {
25
- // An open editor closes, and reloads the page, on a mousedown outside it.
26
- button.addEventListener("mousedown", (event) => event.stopPropagation());
27
25
  button.addEventListener("click", async () => {
28
26
  button.disabled = true;
29
27
  const response = await fetch(button.dataset.url, {
@@ -1,35 +1,28 @@
1
1
  let detach = () => {};
2
2
 
3
- export function attach(note, openNow) {
3
+ export function attach(note, focus) {
4
4
  detach();
5
5
  const prose = note.querySelector(".prose");
6
6
  const status = note.querySelector(".edit-status");
7
- const editor = import(note.dataset.editor);
8
7
  const source = JSON.parse(note.querySelector(".note-source").textContent);
9
8
  let editing = null;
10
9
  let attached = true;
11
10
 
12
- editor.then(
13
- () => prose.classList.add("editable"),
11
+ import(note.dataset.editor).then(
12
+ ({ edit }) => {
13
+ if (!attached) return;
14
+ editing = edit(note, prose, status);
15
+ if (focus) editing.view.focus();
16
+ },
14
17
  () => {},
15
18
  );
16
19
 
17
- const open = async (event) => {
18
- if (event.target.closest("a, button, output") || getSelection().toString()) return;
19
- const { edit } = await editor;
20
- if (editing || !attached) return;
21
- prose.removeEventListener("click", open);
22
- editing = edit(note, prose, status);
23
- };
24
- prose.addEventListener("click", open);
25
- if (openNow) open({ target: prose });
26
-
27
20
  const changes = new EventSource(note.dataset.changesUrl);
28
21
  changes.addEventListener("message", (event) => {
29
22
  if (JSON.parse(event.data) !== note.dataset.note) return;
30
23
  if (editing) return editing.pull();
31
- // A touch or a duplicate event leaves the text alone, and reloading then
32
- // would swallow a click that is opening the editor.
24
+ // Without the editor the note is rendered HTML. A touch or a duplicate
25
+ // event leaves the text alone, and reloading then is wasted.
33
26
  fetch(note.dataset.saveUrl, { cache: "no-store" })
34
27
  .then((response) => response.text())
35
28
  .then((text) => {
@@ -4,12 +4,14 @@ import {
4
4
  HighlightStyle,
5
5
  Language,
6
6
  defineLanguageFacet,
7
+ ensureSyntaxTree,
7
8
  languageDataProp,
8
9
  syntaxHighlighting,
9
10
  syntaxTree,
10
11
  } from "@codemirror/language";
11
- import { EditorState, RangeSetBuilder } from "@codemirror/state";
12
- import { Decoration, EditorView, ViewPlugin, keymap } from "@codemirror/view";
12
+ import { EditorState, StateField } from "@codemirror/state";
13
+ import { Decoration, EditorView, WidgetType, keymap } from "@codemirror/view";
14
+ import { inlinePreview } from "@atomic-editor/editor/inline-preview";
13
15
  import { tags } from "@lezer/highlight";
14
16
  import { GFM, parseCode, parser } from "@lezer/markdown";
15
17
  import { DIFF_DELETE, DIFF_EQUAL, diff_match_patch } from "diff-match-patch";
@@ -31,16 +33,6 @@ const markdown = new Language(
31
33
  );
32
34
 
33
35
  const style = HighlightStyle.define([
34
- { tag: tags.heading1, class: "md-h1" },
35
- { tag: tags.heading2, class: "md-h2" },
36
- { tag: tags.heading3, class: "md-h3" },
37
- { tag: [tags.heading4, tags.heading5, tags.heading6], class: "md-h4" },
38
- { tag: tags.strong, class: "md-strong" },
39
- { tag: tags.emphasis, class: "md-em" },
40
- { tag: tags.strikethrough, class: "md-strike" },
41
- { tag: [tags.link, tags.url], class: "md-link" },
42
- { tag: tags.monospace, class: "md-code" },
43
- { tag: tags.quote, class: "md-quote" },
44
36
  { tag: [tags.processingInstruction, tags.contentSeparator], class: "md-mark" },
45
37
  { tag: [tags.keyword, tags.operatorKeyword, tags.bool, tags.null], class: "py-keyword" },
46
38
  { tag: [tags.string, tags.special(tags.string)], class: "py-string" },
@@ -49,54 +41,86 @@ const style = HighlightStyle.define([
49
41
  { tag: [tags.function(tags.variableName), tags.definition(tags.variableName)], class: "py-name" },
50
42
  ]);
51
43
 
52
- // Markup is hidden except on the lines the cursor is on, as in Obsidian.
53
- const HIDDEN = new Set(["HeaderMark", "EmphasisMark", "CodeMark", "StrikethroughMark"]);
54
- const hide = Decoration.replace({});
55
- const codeLine = Decoration.line({ class: "md-code-line" });
56
-
57
- function decorate(view) {
58
- const builder = new RangeSetBuilder();
59
- const { state } = view;
60
- const active = new Set();
61
- for (const range of state.selection.ranges) {
62
- for (let n = state.doc.lineAt(range.from).number; n <= state.doc.lineAt(range.to).number; n++) active.add(n);
44
+ class RunButton extends WidgetType {
45
+ constructor(block, run) {
46
+ super();
47
+ this.block = block;
48
+ this.run = run;
63
49
  }
64
- for (const { from, to } of view.visibleRanges) {
65
- syntaxTree(state).iterate({
66
- from,
67
- to,
68
- enter(node) {
69
- if (node.name === "FencedCode") {
70
- for (let at = node.from; at <= node.to; ) {
71
- const line = state.doc.lineAt(at);
72
- builder.add(line.from, line.from, codeLine);
73
- at = line.to + 1;
74
- }
75
- return false;
76
- }
77
- if (!HIDDEN.has(node.name) || active.has(state.doc.lineAt(node.from).number)) return;
78
- let end = node.to;
79
- if (node.name === "HeaderMark" && state.doc.sliceString(end, end + 1) === " ") end += 1;
80
- builder.add(node.from, end, hide);
81
- },
82
- });
50
+ eq(other) {
51
+ return other.block === this.block;
52
+ }
53
+ toDOM() {
54
+ const button = document.createElement("button");
55
+ button.type = "button";
56
+ button.className = "run";
57
+ button.textContent = "Run";
58
+ button.addEventListener("click", () => this.run(this.block, button));
59
+ return button;
60
+ }
61
+ ignoreEvent() {
62
+ return true;
83
63
  }
84
- return builder.finish();
85
64
  }
86
65
 
87
- const livePreview = ViewPlugin.fromClass(
88
- class {
89
- constructor(view) {
90
- this.decorations = decorate(view);
66
+ class CellOutput extends WidgetType {
67
+ constructor(block, outputs) {
68
+ super();
69
+ this.block = block;
70
+ this.outputs = outputs;
71
+ }
72
+ eq(other) {
73
+ return other.block === this.block;
74
+ }
75
+ toDOM() {
76
+ const cell = document.createElement("div");
77
+ cell.className = "cell-below";
78
+ if (!this.outputs.has(this.block)) {
79
+ const output = document.createElement("output");
80
+ output.className = "cell-output";
81
+ this.outputs.set(this.block, output);
91
82
  }
92
- update(update) {
93
- if (update.docChanged || update.selectionSet || update.viewportChanged) {
94
- this.decorations = decorate(update.view);
83
+ cell.append(this.outputs.get(this.block));
84
+ return cell;
85
+ }
86
+ ignoreEvent() {
87
+ return true;
88
+ }
89
+ }
90
+
91
+ // Numbered in document order, as python_blocks numbers them on the server.
92
+ function cellWidgets(state, run, outputs) {
93
+ const widgets = [];
94
+ let block = 0;
95
+ const tree = ensureSyntaxTree(state, state.doc.length, 200) ?? syntaxTree(state);
96
+ tree.iterate({
97
+ enter(node) {
98
+ if (node.name !== "FencedCode") return;
99
+ const info = node.node.getChild("CodeInfo");
100
+ if (info && state.doc.sliceString(info.from, info.to).split(/\s/)[0] === "python") {
101
+ const open = state.doc.lineAt(node.from);
102
+ widgets.push(Decoration.widget({ widget: new RunButton(block, run), side: 1 }).range(open.to));
103
+ widgets.push(
104
+ Decoration.widget({ widget: new CellOutput(block, outputs), block: true, side: 1 }).range(node.to),
105
+ );
106
+ block += 1;
95
107
  }
96
- }
97
- },
98
- { decorations: (plugin) => plugin.decorations },
99
- );
108
+ return false;
109
+ },
110
+ });
111
+ return Decoration.set(widgets, true);
112
+ }
113
+
114
+ function cells(run, outputs) {
115
+ return StateField.define({
116
+ create: (state) => cellWidgets(state, run, outputs),
117
+ update(widgets, tr) {
118
+ if (!tr.docChanged && syntaxTree(tr.state) === syntaxTree(tr.startState)) return widgets;
119
+ return cellWidgets(tr.state, run, outputs);
120
+ },
121
+ provide: (field) => EditorView.decorations.from(field),
122
+ });
123
+ }
100
124
 
101
125
  function show(status, message) {
102
126
  status.textContent = message;
@@ -196,15 +220,33 @@ export function edit(note, prose, status) {
196
220
 
197
221
  const save = () => run(push);
198
222
 
199
- const close = async () => {
200
- await save();
201
- document.removeEventListener("mousedown", outside);
202
- location.reload();
203
- };
204
- const outside = (event) => {
205
- if (!note.contains(event.target)) close();
223
+ // The rendered note's outputs move into the editor, so a run started
224
+ // before the editor loaded still shows its result.
225
+ const outputs = new Map();
226
+ for (const cell of prose.querySelectorAll(".cell")) {
227
+ outputs.set(Number(cell.dataset.block), cell.querySelector(".cell-output"));
228
+ }
229
+
230
+ // The server runs the block from the file, so unsaved edits go first.
231
+ const runCell = async (block, button) => {
232
+ button.disabled = true;
233
+ button.textContent = "Running";
234
+ try {
235
+ await save();
236
+ const response = await fetch(note.dataset.runUrl, {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify({ block }),
240
+ });
241
+ outputs.get(block).innerHTML = response.ok
242
+ ? await response.text()
243
+ : `<pre class="error">${response.status} ${response.statusText}</pre>`;
244
+ view.requestMeasure();
245
+ } finally {
246
+ button.disabled = false;
247
+ button.textContent = "Run";
248
+ }
206
249
  };
207
- document.addEventListener("mousedown", outside);
208
250
 
209
251
  const view = new EditorView({
210
252
  parent: host,
@@ -212,10 +254,15 @@ export function edit(note, prose, status) {
212
254
  doc: base,
213
255
  extensions: [
214
256
  history(),
215
- keymap.of([{ key: "Escape", run: () => (close(), true) }, ...defaultKeymap, ...historyKeymap]),
257
+ keymap.of([
258
+ { key: "Escape", run: () => (view.contentDOM.blur(), true) },
259
+ ...defaultKeymap,
260
+ ...historyKeymap,
261
+ ]),
216
262
  markdown,
217
263
  syntaxHighlighting(style),
218
- livePreview,
264
+ inlinePreview({ onLinkClick: (url) => location.assign(new URL(url, location.href)) }),
265
+ cells(runCell, outputs),
219
266
  EditorView.lineWrapping,
220
267
  EditorView.contentAttributes.of({ "aria-label": "Note" }),
221
268
  EditorView.updateListener.of((update) => {
@@ -227,12 +274,9 @@ export function edit(note, prose, status) {
227
274
  ],
228
275
  }),
229
276
  });
230
- view.focus();
231
277
  return {
278
+ view,
232
279
  pull: () => run(pull),
233
- detach: () => {
234
- document.removeEventListener("mousedown", outside);
235
- save();
236
- },
280
+ detach: () => save(),
237
281
  };
238
282
  }
@@ -2,6 +2,7 @@
2
2
  <script type="importmap">
3
3
  {
4
4
  "imports": {
5
+ "@atomic-editor/editor/inline-preview": "https://esm.sh/*@atomic-editor/editor@0.6.2/dist/inline-preview.js",
5
6
  "@codemirror/autocomplete": "https://esm.sh/*@codemirror/autocomplete@6.20.4",
6
7
  "@codemirror/commands": "https://esm.sh/*@codemirror/commands@6.11.1",
7
8
  "@codemirror/lang-python": "https://esm.sh/*@codemirror/lang-python@6.2.1",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes