qword 1.0.2 → 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 (74) hide show
  1. package/ChangeLog +22 -0
  2. package/README.md +2 -3
  3. package/assets/style.css +10 -10
  4. package/client/_clipboard.js +30 -33
  5. package/client/_clipboard.spec.js +160 -0
  6. package/client/content.d.ts +8 -0
  7. package/client/content.js +14 -0
  8. package/client/content.spec.js +69 -0
  9. package/client/create.d.ts +32 -0
  10. package/client/create.js +95 -0
  11. package/client/create.spec.js +192 -0
  12. package/client/decorations.d.ts +40 -0
  13. package/client/decorations.js +110 -0
  14. package/client/decorations.spec.js +135 -0
  15. package/client/dom.d.ts +7 -0
  16. package/client/dom.js +17 -0
  17. package/client/dom.spec.js +78 -0
  18. package/client/editor.d.ts +72 -0
  19. package/client/editor.js +164 -0
  20. package/client/editor.spec.js +307 -0
  21. package/client/events.d.ts +7 -0
  22. package/client/events.js +8 -0
  23. package/client/events.spec.js +100 -0
  24. package/client/highlight.d.ts +3 -0
  25. package/client/highlight.js +82 -0
  26. package/client/highlight.spec.js +28 -0
  27. package/client/history.d.ts +4 -0
  28. package/client/history.js +16 -0
  29. package/client/history.spec.js +57 -0
  30. package/client/index.d.ts +60 -0
  31. package/client/index.js +37 -0
  32. package/client/loadremote.js +10 -4
  33. package/client/options.d.ts +30 -0
  34. package/client/options.js +59 -0
  35. package/client/options.spec.js +157 -0
  36. package/client/position.d.ts +21 -0
  37. package/client/position.js +33 -0
  38. package/client/position.spec.js +176 -0
  39. package/client/qword.js +141 -391
  40. package/client/save.js +2 -2
  41. package/client/scroll.d.ts +10 -0
  42. package/client/scroll.js +9 -0
  43. package/client/scroll.spec.js +58 -0
  44. package/client/types.d.ts +24 -0
  45. package/dist/qword.js +175 -1
  46. package/dist/qword.js.map +1 -1
  47. package/dist-dev/qword.js +544 -660
  48. package/package.json +30 -15
  49. package/server/edit.js +8 -3
  50. package/server/edit.spec.js +101 -0
  51. package/test/errors.ts +21 -0
  52. package/test/types.ts +134 -0
  53. package/client/codemirror/show-trailing.js +0 -54
  54. package/client/codemirror/use-soft-tabs.js +0 -38
  55. package/client/set-key-map.js +0 -3
  56. package/dist/2b22a33a0a80bb926bbc.png +0 -1
  57. package/dist/8e9019bd86255ac9a618.png +0 -1
  58. package/dist/codemirror.js +0 -2
  59. package/dist/codemirror.js.map +0 -1
  60. package/dist/dword.js +0 -2
  61. package/dist/dword.js.map +0 -1
  62. package/dist-dev/2b22a33a0a80bb926bbc.png +0 -1
  63. package/dist-dev/8e9019bd86255ac9a618.png +0 -1
  64. package/dist-dev/codemirror.js +0 -104
  65. package/dist-dev/dword.js +0 -960
  66. package/modules/codemirror-emmet/dist/emmet.js +0 -48661
  67. package/modules/codemirror-emmet/dist/emmet.min.js +0 -8
  68. package/modules/codemirror-emmet/dist/emmetPlugin.js +0 -840
  69. package/modules/codemirror-emmet/dist/emmetPlugin.min.js +0 -1
  70. package/modules/codemirror-emmet/dist/index.html +0 -46
  71. package/modules/jshint/README.md +0 -114
  72. package/modules/jshint/dist/jshint-rhino.js +0 -32143
  73. package/modules/jshint/dist/jshint.js +0 -32141
  74. package/modules/jshint/package.json +0 -77
package/client/qword.js CHANGED
@@ -1,433 +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
- // ✅ FIX: добавили vim 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`);
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
+ }
120
57
 
121
- this._initEditor();
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;
67
+ }
122
68
 
123
- if (this._pendingValue !== null) {
124
- this.setValue(this._pendingValue);
125
- this._pendingValue = null;
69
+ sha() {
70
+ const sha = new jssha('SHA-1', 'TEXT');
71
+ sha.update(this.getValue());
72
+
73
+ return sha.getHash('HEX');
126
74
  }
127
75
 
128
- this._initSocket();
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
+ }
129
87
 
130
- return this;
131
- };
132
-
133
- Qword.prototype._initEditor = function() {
134
- this._Element.style.height = '100vh';
135
- const baseExtensions = () => [
136
- lineNumbers(),
137
- history(),
138
- highlightActiveLine(),
139
- syntaxHighlighting(defaultHighlightStyle),
140
- oneDark,
141
- this._langCompartment.of(javascript()),
142
- // ✅ FIX: vim теперь через compartment
143
- this._vimCompartment.of([]),
144
- keymap.of([
145
- ...defaultKeymap,
146
- ...historyKeymap, {
147
- key: 'Mod-s',
148
- run: () => {
149
- this.save();
150
- return true;
151
- },
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,
152
98
  },
153
- ]),
154
- ];
155
-
156
- this._vimEnabled = false;
157
-
158
- const state = EditorState.create({
159
- doc: '',
160
- extensions: baseExtensions(),
161
- });
162
-
163
- this._view = new EditorView({
164
- state,
165
- parent: this._Element,
166
- });
99
+ scrollIntoView: true,
100
+ });
101
+ this.focus();
102
+ }
167
103
 
168
- this._baseExtensions = baseExtensions;
169
- };
170
-
171
- Qword.prototype.getValue = function() {
172
- return this._view.state.doc.toString();
173
- };
174
-
175
- Qword.prototype.setValue = function(value) {
176
- if (!this._view) {
177
- 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
+
178
131
  return this;
179
132
  }
180
133
 
181
- this._view.dispatch({
182
- changes: {
183
- from: 0,
184
- to: this._view.state.doc.length,
185
- insert: value,
186
- },
187
- });
188
-
189
- return this;
190
- };
191
-
192
- Qword.prototype.focus = function() {
193
- this._view.focus();
194
- return this;
195
- };
196
-
197
- Qword.prototype.getCursor = function() {
198
- const pos = this._view.state.selection.main.head;
199
- const line = this._view.state.doc.lineAt(pos);
200
-
201
- return {
202
- row: line.number - 1,
203
- column: pos - line.from,
204
- };
205
- };
206
-
207
- Qword.prototype.moveCursorTo = function(row, column = 0) {
208
- const line = this._view.state.doc.line(row + 1);
209
-
210
- this._view.dispatch({
211
- selection: {
212
- anchor: line.from + column,
213
- },
214
- scrollIntoView: true,
215
- });
216
-
217
- return this;
218
- };
219
-
220
- Qword.prototype.remove = function(direction) {
221
- const pos = this._view.state.selection.main.head;
222
-
223
- this._view.dispatch({
224
- changes: direction === 'right' ? {
225
- from: pos,
226
- to: pos + 1,
227
- } : {
228
- from: pos - 1,
229
- to: pos,
230
- },
231
- });
232
-
233
- return this;
234
- };
235
-
236
- Qword.prototype.setModeForPath = function(path) {
237
- const ext = path
238
- .split('.')
239
- .pop();
240
-
241
- let lang = javascript();
242
-
243
- if (ext === 'json')
244
- lang = json();
245
-
246
- if (ext === 'html')
247
- lang = html();
248
-
249
- this._view.dispatch({
250
- effects: this._langCompartment.reconfigure(lang),
251
- });
252
-
253
- return this;
254
- };
255
-
256
- Qword.prototype.setValueFirst = function(name, value) {
257
- this.setModeForPath(name);
258
- this.setValue(value);
259
-
260
- this._FileName = name;
261
- this._Value = value;
262
-
263
- this.moveCursorTo(0, 0);
264
-
265
- return this;
266
- };
267
-
268
- Qword.prototype.addKeyMap = function(map) {
269
- const bindings = Object
270
- .entries(map)
271
- .map(([key, fn]) => ({
272
- key,
273
- run: () => {
274
- fn.call(this);
275
- return true;
134
+ selectAll() {
135
+ const {doc} = this._view.state;
136
+ this._view.dispatch({
137
+ selection: {
138
+ anchor: 0,
139
+ head: doc.length,
276
140
  },
277
- }));
278
-
279
- this._view.dispatch({
280
- effects: this._keymapCompartment.reconfigure(keymap.of(bindings)),
281
- });
282
-
283
- return this;
284
- };
285
-
286
- Qword.prototype.on = function(event, fn) {
287
- this._Emitter.on(event, fn);
288
- return this;
289
- };
290
-
291
- Qword.prototype.emit = function(...args) {
292
- this._Emitter.emit(...args);
293
- return this;
294
- };
295
-
296
- Qword.prototype.isChanged = function() {
297
- return this.getValue() !== this._Value;
298
- };
299
-
300
- Qword.prototype._diff = function(newValue) {
301
- return createPatch(this._Value || '', newValue);
302
- };
303
-
304
- Qword.prototype._patchHttp = function(path, patch) {
305
- restafary.patch(path, patch, this._onSave.bind(this));
306
- };
307
-
308
- Qword.prototype._writeHttp = function(path, result) {
309
- restafary.write(path, result, this._onSave.bind(this));
310
- };
311
-
312
- Qword.prototype.evaluate = function() {
313
- if (!this._FileName?.endsWith('.js'))
314
- return smalltalk.alert(this._TITLE, 'JS only');
141
+ });
142
+
143
+ return this;
144
+ }
315
145
 
316
- const [e] = tryCatch(new Function(this.getValue()));
146
+ enableKey() {
147
+ return this;
148
+ }
317
149
 
318
- if (e)
319
- smalltalk.alert(this._TITLE, e.message);
150
+ cutToClipboard() {
151
+ return this._clipboard('cut');
152
+ }
320
153
 
321
- return this;
322
- };
323
-
324
- Qword.prototype.goToLine = function() {
325
- const num = Number(prompt('Line number'));
154
+ copyToClipboard() {
155
+ return this._clipboard('copy');
156
+ }
326
157
 
327
- if (!num)
328
- return;
158
+ pasteFromClipboard() {
159
+ return this._clipboard('paste');
160
+ }
329
161
 
330
- const line = this._view.state.doc.line(num);
162
+ _patchHttp(path, patch) {
163
+ restafary.patch(path, patch, this._onSave.bind(this));
164
+ }
331
165
 
332
- this._view.dispatch({
333
- selection: {
334
- anchor: line.from,
335
- },
336
- scrollIntoView: true,
337
- });
166
+ _writeHttp(path, data) {
167
+ restafary.write(path, data, this._onSave.bind(this));
168
+ }
338
169
 
339
- this.focus();
340
- };
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
+ }
341
178
 
342
179
  Qword.prototype._clipboard = _clipboard;
343
180
  Qword.prototype.save = save;
344
181
  Qword.prototype._onSave = _onSave;
345
182
  Qword.prototype._initSocket = _initSocket;
346
-
347
- Qword.prototype._onDragOver = function(event) {
348
- event.preventDefault();
349
- event.dataTransfer.dropEffect = 'copy';
350
- };
351
-
352
- Qword.prototype._onDrop = function(event) {
353
- event.preventDefault();
354
-
355
- const [file] = event.dataTransfer.files;
356
-
357
- if (!file)
358
- return;
359
-
360
- const reader = new FileReader();
361
-
362
- reader.onload = (e) => {
363
- this.setValue(e.target.result);
364
- };
365
-
366
- reader.readAsText(file);
367
- };
368
-
369
- Qword.prototype.sha = function() {
370
- const shaObj = new jssha('SHA-1', 'TEXT');
371
- shaObj.update(this.getValue());
372
-
373
- return shaObj.getHash('HEX');
374
- };
375
-
376
- Qword.prototype.setOption = function(name, value) {
377
- const {_Ace} = this;
378
- const preventOverwrite = () => {
379
- this._Config.options[name] = value;
380
- };
381
-
382
- preventOverwrite();
383
-
384
- if (name === 'keyMap')
385
- this.setKeyMap(value);
386
-
387
- return this;
388
- };
389
-
390
- Qword.prototype.setOptions = function(options) {
391
- const theme = EditorView.theme({
392
- '&': {
393
- fontSize: options.fontSize,
394
- fontFamily: options.fontFamily,
395
- },
396
- });
397
-
398
- this._view.dispatch({
399
- effects: this._fontCompartment.reconfigure(theme),
400
- });
401
-
402
- return this;
403
- };
404
-
405
183
  Qword.prototype.showMessage = showMessage;
406
-
407
- Qword.prototype.setKeyMap = function(mode) {
408
- this._vimEnabled = mode === 'vim';
409
-
410
- this._view.dispatch({
411
- effects: this._vimCompartment.reconfigure(this._vimEnabled ? vim() : []),
412
- });
413
-
414
- return this;
415
- };
416
-
417
- Qword.prototype._loadOptions = async function() {
418
- const url = this._PREFIX + '/options.json';
419
-
420
- if (this._Options)
421
- return this._Options;
422
-
423
- const data = await load.json(url);
424
-
425
- this._Options = data;
426
-
427
- return data;
428
- };
429
-
430
- Qword.prototype.enableKey = function() {
431
- this._isKey = true;
432
- return this;
433
- };
package/client/save.js CHANGED
@@ -34,9 +34,9 @@ async function save() {
34
34
  if (!zip)
35
35
  return this._write(_filename, value);
36
36
 
37
- const zipedValue = await zipio(value);
37
+ const zippedValue = await zipio(value);
38
38
 
39
- return this._write(`${_filename}?unzip`, zipedValue);
39
+ return this._write(`${_filename}?unzip`, zippedValue);
40
40
  }
41
41
 
42
42
  function _setValue(ctx) {
@@ -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;