qword 1.0.3 → 1.1.0

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 (72) hide show
  1. package/ChangeLog +14 -0
  2. package/assets/style.css +10 -10
  3. package/client/_clipboard.js +30 -33
  4. package/client/_clipboard.spec.js +160 -0
  5. package/client/content.d.ts +8 -0
  6. package/client/content.js +14 -0
  7. package/client/content.spec.js +69 -0
  8. package/client/create.d.ts +32 -0
  9. package/client/create.js +95 -0
  10. package/client/create.spec.js +192 -0
  11. package/client/decorations.d.ts +40 -0
  12. package/client/decorations.js +110 -0
  13. package/client/decorations.spec.js +135 -0
  14. package/client/dom.d.ts +7 -0
  15. package/client/dom.js +17 -0
  16. package/client/dom.spec.js +78 -0
  17. package/client/editor.d.ts +72 -0
  18. package/client/editor.js +164 -0
  19. package/client/editor.spec.js +307 -0
  20. package/client/events.d.ts +7 -0
  21. package/client/events.js +8 -0
  22. package/client/events.spec.js +100 -0
  23. package/client/highlight.d.ts +3 -0
  24. package/client/highlight.js +82 -0
  25. package/client/highlight.spec.js +28 -0
  26. package/client/history.d.ts +4 -0
  27. package/client/history.js +16 -0
  28. package/client/history.spec.js +57 -0
  29. package/client/index.d.ts +60 -0
  30. package/client/index.js +37 -0
  31. package/client/loadremote.js +10 -4
  32. package/client/options.d.ts +30 -0
  33. package/client/options.js +59 -0
  34. package/client/options.spec.js +157 -0
  35. package/client/position.d.ts +21 -0
  36. package/client/position.js +33 -0
  37. package/client/position.spec.js +176 -0
  38. package/client/qword.js +141 -416
  39. package/client/scroll.d.ts +10 -0
  40. package/client/scroll.js +9 -0
  41. package/client/scroll.spec.js +58 -0
  42. package/client/types.d.ts +24 -0
  43. package/dist/qword.js +175 -1
  44. package/dist/qword.js.map +1 -1
  45. package/dist-dev/qword.js +544 -660
  46. package/package.json +30 -14
  47. package/server/edit.js +8 -3
  48. package/server/edit.spec.js +101 -0
  49. package/test/errors.ts +21 -0
  50. package/test/types.ts +134 -0
  51. package/client/codemirror/show-trailing.js +0 -54
  52. package/client/codemirror/use-soft-tabs.js +0 -38
  53. package/client/set-key-map.js +0 -3
  54. package/dist/2b22a33a0a80bb926bbc.png +0 -1
  55. package/dist/8e9019bd86255ac9a618.png +0 -1
  56. package/dist/codemirror.js +0 -2
  57. package/dist/codemirror.js.map +0 -1
  58. package/dist/dword.js +0 -2
  59. package/dist/dword.js.map +0 -1
  60. package/dist-dev/2b22a33a0a80bb926bbc.png +0 -1
  61. package/dist-dev/8e9019bd86255ac9a618.png +0 -1
  62. package/dist-dev/codemirror.js +0 -104
  63. package/dist-dev/dword.js +0 -960
  64. package/modules/codemirror-emmet/dist/emmet.js +0 -48661
  65. package/modules/codemirror-emmet/dist/emmet.min.js +0 -8
  66. package/modules/codemirror-emmet/dist/emmetPlugin.js +0 -840
  67. package/modules/codemirror-emmet/dist/emmetPlugin.min.js +0 -1
  68. package/modules/codemirror-emmet/dist/index.html +0 -46
  69. package/modules/jshint/README.md +0 -114
  70. package/modules/jshint/dist/jshint-rhino.js +0 -32143
  71. package/modules/jshint/dist/jshint.js +0 -32141
  72. package/modules/jshint/package.json +0 -77
package/client/qword.js CHANGED
@@ -1,458 +1,183 @@
1
1
  import '../css/qword.css';
2
- import {EditorState, Compartment} from '@codemirror/state';
3
- import {
4
- EditorView,
5
- keymap,
6
- lineNumbers,
7
- highlightActiveLine,
8
- } from '@codemirror/view';
9
- import {
10
- history,
11
- historyKeymap,
12
- defaultKeymap,
13
- } from '@codemirror/commands';
14
- import {
15
- syntaxHighlighting,
16
- defaultHighlightStyle,
17
- } from '@codemirror/language';
18
- import {oneDark} from '@codemirror/theme-one-dark';
19
- import {javascript} from '@codemirror/lang-javascript';
20
- import {json} from '@codemirror/lang-json';
21
- import {html} from '@codemirror/lang-html';
22
- import Emitify from 'emitify';
23
2
  import * as smalltalk from 'smalltalk';
24
3
  import jssha from 'jssha';
25
4
  import load from 'load.js';
26
5
  import {createPatch} from 'daffy';
27
6
  import * as restafary from 'restafary/client';
28
- import {tryCatch} from 'try-catch';
29
- import {vim} from '@replit/codemirror-vim';
30
7
  import {tryToCatch} from 'try-to-catch';
8
+ import {tryCatch} from 'try-catch';
9
+ import Editor from './editor.js';
31
10
  import Story from './story.js';
32
- import loadRemote, {loadModules, loadOptions} from './loadremote.js';
33
11
  import _clipboard from './_clipboard.js';
34
12
  import save from './save.js';
35
13
  import _initSocket from './_init-socket.js';
36
14
  import _onSave from './_on-save.js';
37
15
  import showMessage from './show-message.js';
16
+ import loadRemote, {loadModules, loadOptions} from './loadremote.js';
38
17
 
39
18
  const noop = () => {};
40
19
 
41
- const isFn = (a) => typeof a === 'function';
42
- const isString = (a) => typeof a === 'string';
43
-
44
- export default function Qword(el, options = {}, callback = noop) {
45
- if (!(this instanceof Qword))
46
- return new Qword(el, options, callback);
47
-
48
- if (isFn(options)) {
49
- callback = options;
50
- options = {};
20
+ export default class Qword extends Editor {
21
+ constructor(element, options = {}, callback = noop) {
22
+ super(element, options);
23
+
24
+ this._TITLE = 'Qword';
25
+ this._story = Story();
26
+ this._savedValue = '';
27
+ this._filename = '';
28
+ this._maxSize = options.maxSize || 512_000;
29
+ this._PREFIX = options.prefix || '/qword';
30
+ this._prefixSocket = options.prefixSocket || '/qword';
31
+ this._socketPath = options.socketPath || '';
32
+
33
+ restafary.prefix(`${this._PREFIX}/api/v1/fs`);
34
+ this
35
+ ._init()
36
+ .then(() => callback(this));
51
37
  }
52
38
 
53
- this._DIR = '/modules/';
54
- this._TITLE = 'Qword';
55
-
56
- this._story = Story();
57
-
58
- this._Separator = '\n';
59
- this._isKey = true;
60
-
61
- this._Element = isString(el) ? document.querySelector(el) : el || document.body;
62
-
63
- if (!this._Element)
64
- throw Error('Qword: element not found');
65
-
66
- this._maxSize = options.maxSize || 512_000;
67
-
68
- this._PREFIX = options.prefix || '/qword';
69
- this._prefixSocket = options.prefixSocket || '/qword';
70
- this._socketPath = options.socketPath || '';
71
-
72
- this._Emitter = Emitify();
73
-
74
- this._view = null;
75
- this._pendingValue = null;
76
-
77
- this._modeCompartment = new Compartment();
78
- this._keymapCompartment = new Compartment();
79
- this._fontCompartment = new Compartment();
80
- this._langCompartment = new Compartment();
81
- this._historyCompartment = new Compartment();
82
- this._vimCompartment = new Compartment();
83
-
84
- this._Element.addEventListener('drop', this._onDrop.bind(this));
85
- this._Element.addEventListener('dragover', this._onDragOver.bind(this));
86
-
87
- this
88
- ._init()
89
- .then(() => callback(this));
90
-
91
- this._patch = (path, patch) => {
92
- this._patchHttp(path, patch);
93
- };
94
-
95
- this._write = (path, result) => {
96
- this._writeHttp(path, result);
97
- };
98
- }
99
-
100
- Qword.prototype._init = async function() {
101
- const prefix = this._PREFIX;
102
-
103
- const [error, config] = await tryToCatch(load.json, this._PREFIX + '/edit.json');
104
-
105
- if (error)
106
- return smalltalk.alert(this._TITLE, 'Could not load edit.json!');
107
-
108
- this._Config = config;
109
-
110
- await Promise.all([
111
- loadOptions(prefix),
112
- loadModules(prefix),
113
- ]);
114
-
115
- await loadRemote('socket', {
116
- prefix: this._socketPath,
117
- });
118
-
119
- restafary.prefix(`${this._PREFIX}/api/v1/fs`);
120
-
121
- this._initEditor();
39
+ async _init() {
40
+ const [error, config] = await tryToCatch(load.json, `${this._PREFIX}/edit.json`);
41
+
42
+ if (error)
43
+ return smalltalk.alert(this._TITLE, 'Could not load edit.json!');
44
+
45
+ this._Config = config;
46
+
47
+ await Promise.all([
48
+ loadOptions(this._PREFIX),
49
+ loadModules(this._PREFIX),
50
+ ]);
51
+
52
+ await loadRemote('socket', {
53
+ prefix: this._socketPath,
54
+ });
55
+ this._initSocket();
56
+ }
122
57
 
123
- if (this._pendingValue !== null) {
124
- this.setValue(this._pendingValue);
125
- this._pendingValue = null;
58
+ setValueFirst(name, value) {
59
+ this.setModeForPath(name);
60
+ this.setValue(value);
61
+ this._filename = name;
62
+ this._savedValue = value;
63
+ this.clearHistory();
64
+ this.moveCursorTo(0, 0);
65
+
66
+ return this;
126
67
  }
127
68
 
128
- this._initSocket();
69
+ sha() {
70
+ const sha = new jssha('SHA-1', 'TEXT');
71
+ sha.update(this.getValue());
72
+
73
+ return sha.getHash('HEX');
74
+ }
129
75
 
130
- return this;
131
- };
132
-
133
- Qword.prototype._initEditor = function() {
134
- this._Element.style.height = '100vh';
76
+ evaluate() {
77
+ if (!this._filename?.endsWith('.js'))
78
+ return smalltalk.alert(this._TITLE, 'JS only');
79
+
80
+ const [error] = tryCatch(new Function(this.getValue()));
81
+
82
+ if (error)
83
+ smalltalk.alert(this._TITLE, error.message);
84
+
85
+ return this;
86
+ }
135
87
 
136
- const baseExtensions = () => [
137
- this._historyCompartment.of([
138
- history(),
139
- keymap.of(historyKeymap),
140
- ]),
141
- lineNumbers(),
142
- history(),
143
- highlightActiveLine(),
144
- syntaxHighlighting(defaultHighlightStyle),
145
- oneDark,
146
- this._langCompartment.of(javascript()),
147
- this._vimCompartment.of([]),
148
- keymap.of([
149
- ...defaultKeymap,
150
- ...historyKeymap, {
151
- key: 'Mod-s',
152
- run: () => {
153
- this.save();
154
- return true;
155
- },
88
+ goToLine() {
89
+ const number = Number(prompt('Line number'));
90
+
91
+ if (!number)
92
+ return;
93
+
94
+ const line = this._view.state.doc.line(number);
95
+ this._view.dispatch({
96
+ selection: {
97
+ anchor: line.from,
156
98
  },
157
- ]),
158
- ];
159
-
160
- this._vimEnabled = false;
161
-
162
- const state = EditorState.create({
163
- doc: '',
164
- extensions: baseExtensions(),
165
- });
166
-
167
- this._view = new EditorView({
168
- state,
169
- parent: this._Element,
170
- });
99
+ scrollIntoView: true,
100
+ });
101
+ this.focus();
102
+ }
171
103
 
172
- this._baseExtensions = baseExtensions;
173
- };
174
-
175
- Qword.prototype.getValue = function() {
176
- return this._view.state.doc.toString();
177
- };
178
-
179
- Qword.prototype.setValue = function(value) {
180
- if (!this._view) {
181
- this._pendingValue = value;
104
+ remove(direction) {
105
+ const {state} = this._view;
106
+ const {from, to} = state.selection.main;
107
+ const isCollapsed = from === to;
108
+
109
+ if (direction === 'right') {
110
+ const end = isCollapsed ? to + 1 : to;
111
+ this._view.dispatch({
112
+ changes: {
113
+ from,
114
+ to: end,
115
+ insert: '',
116
+ },
117
+ });
118
+ } else {
119
+ const start = isCollapsed ? from - 1 : from;
120
+
121
+ if (start >= 0)
122
+ this._view.dispatch({
123
+ changes: {
124
+ from: start,
125
+ to,
126
+ insert: '',
127
+ },
128
+ });
129
+ }
130
+
182
131
  return this;
183
132
  }
184
133
 
185
- this._view.dispatch({
186
- changes: {
187
- from: 0,
188
- to: this._view.state.doc.length,
189
- insert: value,
190
- },
191
- });
192
-
193
- return this;
194
- };
195
-
196
- Qword.prototype.focus = function() {
197
- this._view.focus();
198
- return this;
199
- };
200
-
201
- Qword.prototype.getCursor = function() {
202
- const pos = this._view.state.selection.main.head;
203
- const line = this._view.state.doc.lineAt(pos);
204
-
205
- return {
206
- row: line.number - 1,
207
- column: pos - line.from,
208
- };
209
- };
210
-
211
- Qword.prototype.moveCursorTo = function(row, column = 0) {
212
- const line = this._view.state.doc.line(row + 1);
213
-
214
- this._view.dispatch({
215
- selection: {
216
- anchor: line.from + column,
217
- },
218
- scrollIntoView: true,
219
- });
220
-
221
- return this;
222
- };
223
-
224
- Qword.prototype.remove = function(direction) {
225
- const pos = this._view.state.selection.main.head;
226
-
227
- this._view.dispatch({
228
- changes: direction === 'right' ? {
229
- from: pos,
230
- to: pos + 1,
231
- } : {
232
- from: pos - 1,
233
- to: pos,
234
- },
235
- });
236
-
237
- return this;
238
- };
239
-
240
- Qword.prototype.setModeForPath = function(path) {
241
- const ext = path
242
- .split('.')
243
- .pop();
244
-
245
- let lang = javascript();
246
-
247
- if (ext === 'json')
248
- lang = json();
249
-
250
- if (ext === 'html')
251
- lang = html();
252
-
253
- this._view.dispatch({
254
- effects: this._langCompartment.reconfigure(lang),
255
- });
256
-
257
- return this;
258
- };
259
-
260
- Qword.prototype.setValueFirst = function(name, value) {
261
- this.setModeForPath(name);
262
- this.setValue(value);
263
-
264
- this._FileName = name;
265
- this._Value = value;
266
-
267
- this.clearHistory();
268
- this.moveCursorTo(0, 0);
269
-
270
- return this;
271
- };
272
-
273
- Qword.prototype.addKeyMap = function(map) {
274
- const bindings = Object
275
- .entries(map)
276
- .map(([key, fn]) => ({
277
- key,
278
- run: () => {
279
- fn.call(this);
280
- return true;
134
+ selectAll() {
135
+ const {doc} = this._view.state;
136
+ this._view.dispatch({
137
+ selection: {
138
+ anchor: 0,
139
+ head: doc.length,
281
140
  },
282
- }));
283
-
284
- this._view.dispatch({
285
- effects: this._keymapCompartment.reconfigure(keymap.of(bindings)),
286
- });
287
-
288
- return this;
289
- };
290
-
291
- Qword.prototype.on = function(event, fn) {
292
- this._Emitter.on(event, fn);
293
- return this;
294
- };
295
-
296
- Qword.prototype.emit = function(...args) {
297
- this._Emitter.emit(...args);
298
- return this;
299
- };
300
-
301
- Qword.prototype.isChanged = function() {
302
- return this.getValue() !== this._Value;
303
- };
304
-
305
- Qword.prototype._diff = function(newValue) {
306
- return createPatch(this._Value || '', newValue);
307
- };
308
-
309
- Qword.prototype._patchHttp = function(path, patch) {
310
- restafary.patch(path, patch, this._onSave.bind(this));
311
- };
312
-
313
- Qword.prototype._writeHttp = function(path, result) {
314
- restafary.write(path, result, this._onSave.bind(this));
315
- };
316
-
317
- Qword.prototype.evaluate = function() {
318
- if (!this._FileName?.endsWith('.js'))
319
- return smalltalk.alert(this._TITLE, 'JS only');
141
+ });
142
+
143
+ return this;
144
+ }
320
145
 
321
- const [e] = tryCatch(new Function(this.getValue()));
146
+ enableKey() {
147
+ return this;
148
+ }
322
149
 
323
- if (e)
324
- smalltalk.alert(this._TITLE, e.message);
150
+ cutToClipboard() {
151
+ return this._clipboard('cut');
152
+ }
325
153
 
326
- return this;
327
- };
328
-
329
- Qword.prototype.goToLine = function() {
330
- const num = Number(prompt('Line number'));
154
+ copyToClipboard() {
155
+ return this._clipboard('copy');
156
+ }
331
157
 
332
- if (!num)
333
- return;
158
+ pasteFromClipboard() {
159
+ return this._clipboard('paste');
160
+ }
334
161
 
335
- const line = this._view.state.doc.line(num);
162
+ _patchHttp(path, patch) {
163
+ restafary.patch(path, patch, this._onSave.bind(this));
164
+ }
336
165
 
337
- this._view.dispatch({
338
- selection: {
339
- anchor: line.from,
340
- },
341
- scrollIntoView: true,
342
- });
166
+ _writeHttp(path, data) {
167
+ restafary.write(path, data, this._onSave.bind(this));
168
+ }
343
169
 
344
- this.focus();
345
- };
170
+ async _doDiff(path) {
171
+ const equal = await this
172
+ ._story
173
+ .checkHash(path)
174
+ .catch(() => false);
175
+ return equal ? '' : createPatch(this._savedValue || '', this.getValue());
176
+ }
177
+ }
346
178
 
347
179
  Qword.prototype._clipboard = _clipboard;
348
180
  Qword.prototype.save = save;
349
181
  Qword.prototype._onSave = _onSave;
350
182
  Qword.prototype._initSocket = _initSocket;
351
-
352
- Qword.prototype._onDragOver = function(event) {
353
- event.preventDefault();
354
- event.dataTransfer.dropEffect = 'copy';
355
- };
356
-
357
- Qword.prototype._onDrop = function(event) {
358
- event.preventDefault();
359
-
360
- const [file] = event.dataTransfer.files;
361
-
362
- if (!file)
363
- return;
364
-
365
- const reader = new FileReader();
366
-
367
- reader.onload = (e) => {
368
- this.setValue(e.target.result);
369
- };
370
-
371
- reader.readAsText(file);
372
- };
373
-
374
- Qword.prototype.sha = function() {
375
- const shaObj = new jssha('SHA-1', 'TEXT');
376
- shaObj.update(this.getValue());
377
-
378
- return shaObj.getHash('HEX');
379
- };
380
-
381
- Qword.prototype.setOption = function(name, value) {
382
- const preventOverwrite = () => {
383
- this._Config.options[name] = value;
384
- };
385
-
386
- preventOverwrite();
387
-
388
- if (name === 'keyMap')
389
- this.setKeyMap(value);
390
-
391
- return this;
392
- };
393
-
394
- Qword.prototype.setOptions = function(options) {
395
- const theme = EditorView.theme({
396
- '&': {
397
- fontSize: options.fontSize,
398
- fontFamily: options.fontFamily,
399
- },
400
- });
401
-
402
- this._view.dispatch({
403
- effects: this._fontCompartment.reconfigure(theme),
404
- });
405
-
406
- return this;
407
- };
408
-
409
183
  Qword.prototype.showMessage = showMessage;
410
-
411
- Qword.prototype.setKeyMap = function(mode) {
412
- this._vimEnabled = mode === 'vim';
413
-
414
- this._view.dispatch({
415
- effects: this._vimCompartment.reconfigure(this._vimEnabled ? vim() : []),
416
- });
417
-
418
- return this;
419
- };
420
-
421
- Qword.prototype._loadOptions = async function() {
422
- const url = this._PREFIX + '/options.json';
423
-
424
- if (this._Options)
425
- return this._Options;
426
-
427
- const data = await load.json(url);
428
-
429
- this._Options = data;
430
-
431
- return data;
432
- };
433
-
434
- Qword.prototype.enableKey = function() {
435
- this._isKey = true;
436
- return this;
437
- };
438
-
439
- Qword.prototype.clearHistory = function() {
440
- if (!this._view)
441
- return this;
442
-
443
- this._view.dispatch({
444
- effects: this._historyCompartment.reconfigure(this._vimEnabled ? [] : [
445
- history(),
446
- keymap.of(historyKeymap),
447
- ]),
448
- });
449
-
450
- const {cm} = this._view;
451
-
452
- cm.state.vim.undoHistory = [];
453
- cm.state.vim.redoHistory = [];
454
- cm.state.vim.globalState.jumpList = [];
455
-
456
- return this;
457
- };
458
-
@@ -0,0 +1,10 @@
1
+ import type {EditorView} from '@codemirror/view';
2
+
3
+ export type ScrollInfo = {
4
+ left: number;
5
+ top: number;
6
+ };
7
+
8
+ export const getScrollInfo: (view: EditorView) => ScrollInfo;
9
+
10
+ export function scrollTo(view: EditorView, left: number, top: number): void;
@@ -0,0 +1,9 @@
1
+ export const getScrollInfo = (view) => ({
2
+ left: view.scrollDOM.scrollLeft,
3
+ top: view.scrollDOM.scrollTop,
4
+ });
5
+
6
+ export function scrollTo(view, left, top) {
7
+ view.scrollDOM.scrollLeft = left;
8
+ view.scrollDOM.scrollTop = top;
9
+ }
@@ -0,0 +1,58 @@
1
+ import {test} from 'supertape';
2
+ import {EditorState} from '@codemirror/state';
3
+ import {EditorView} from '@codemirror/view';
4
+ import {getScrollInfo, scrollTo} from './scroll.js';
5
+
6
+ function makeView() {
7
+ const element = document.createElement('div');
8
+ document.body.appendChild(element);
9
+
10
+ return new EditorView({
11
+ state: EditorState.create({
12
+ doc: 'hello',
13
+ }),
14
+ parent: element,
15
+ });
16
+ }
17
+
18
+ test('scroll: getScrollInfo left is a number', (t) => {
19
+ const view = makeView();
20
+ const result = typeof getScrollInfo(view).left;
21
+ const expected = 'number';
22
+
23
+ view.destroy();
24
+
25
+ t.equal(result, expected);
26
+ t.end();
27
+ });
28
+
29
+ test('scroll: getScrollInfo top is a number', (t) => {
30
+ const view = makeView();
31
+ const result = typeof getScrollInfo(view).top;
32
+ const expected = 'number';
33
+
34
+ view.destroy();
35
+
36
+ t.equal(result, expected);
37
+ t.end();
38
+ });
39
+
40
+ test('scroll: scrollTo sets scrollLeft to 0', (t) => {
41
+ const view = makeView();
42
+ scrollTo(view, 0, 0);
43
+
44
+ view.destroy();
45
+
46
+ t.equal(view.scrollDOM.scrollLeft, 0);
47
+ t.end();
48
+ });
49
+
50
+ test('scroll: scrollTo sets scrollTop to 0', (t) => {
51
+ const view = makeView();
52
+ scrollTo(view, 0, 0);
53
+
54
+ view.destroy();
55
+
56
+ t.equal(view.scrollDOM.scrollTop, 0);
57
+ t.end();
58
+ });
@@ -0,0 +1,24 @@
1
+ // Character offset into source text (0-indexed, matches CM6) — same convention as putout-editor's codemirror-adapter.
2
+ export type CharOffset = number;
3
+
4
+ // Line/column position — matches CodeMirror's {line, ch} convention.
5
+ export type SourcePosition = {
6
+ line: number;
7
+ ch: number;
8
+ };
9
+
10
+ // CodeMirror editor mode string — e.g. 'javascript', 'json', 'html'.
11
+ export type EditorMode = string;
12
+
13
+ // CodeMirror key map name.
14
+ export type KeyMap =
15
+ | 'vim'
16
+ | 'emacs'
17
+ | 'sublime'
18
+ | 'default';
19
+
20
+ // Editor theme name.
21
+ export type EditorTheme = 'nord' | 'default' | 'dark' | 'one-dark';
22
+
23
+ // JavaScript/TypeScript source code.
24
+ export type SourceCode = string;