-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathGhostRider.dyalog
1220 lines (1099 loc) · 69.7 KB
/
GhostRider.dyalog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
:Class GhostRider
⍝ Headless RIDE client for QA and automation.
⍝ This class will connect to an APL process (or create a new one)
⍝ and synchronously communicate through the RIDE protocol in order to control it.
⍝ This means that when the GhostRider expects a response from the interpreter
⍝ it will block the APL thread until it gets it.
⍝ Dyalog v18.0 Unicode or later required.
⍝ To create a new APL process and connect to it
⍝ R←⎕NEW GhostRider {env}
⍝ - optional {env} is a string giving a list of environment variables to set up for the interpreter
⍝ e.g. 'MAXWS=1G WSPATH=.'
⍝ defaults to ''
⍝ To connect to an existing process
⍝ R←⎕NEW GhostRider (port {host})
⍝ - port is the positive integer port number to connect to.
⍝ - optional {host} is a string giving the ip address to connect to
⍝ {host} defaults to '127.0.0.1' which is the local machine
⍝ RIDE commands usually wait for a response,
⍝ which may be changing the prompt type, or touching a window (edit, tracer, dialog, etc.).
⍝ This is specified by a 2-element vector :
⍝ <wait> ←→ (waitprompts waitwins)
⍝ - waitprompts is a list of prompt types to wait for
⍝ - waitwins is either a number of windows to wait for, or a list of windows to wait for.
⍝ Both conditions are awaited for (the conjunction is AND and not OR),
⍝ excepted when waiting for no prompt and no window : the first prompt or window that happens returns.
⍝ The default is to wait for any prompt to come back (1 2 3 4)
⍝ and to wait for no window (0) because windows generally pop up before the prompt comes back.
⍝
⍝ RIDE commands return a 4-element result :
⍝ <result> ←→ (prompt output wins errors)
⍝ - prompt is the last prompt that was set (each is integer, see below)
⍝ - output is the string of session output (including newlines)
⍝ - wins is list of opened windows (each is a namespace - see below)
⍝ - errors is the list of ⎕SIGNAL-ed errors (there should be one at most)
⍝
⍝ Valid prompt types are :
⍝ - ¯1 = prompt unset
⍝ - 0 = no prompt
⍝ - 1 = the usual 6-space APL prompt (a.k.a. Descalc or "desktop calculator")
⍝ - 2 = Quad(⎕) input
⍝ - 3 = ∇ line editor
⍝ - 4 = Quote-Quad(⍞) input
⍝ - 5 = any prompt type unforeseen here.
⍝
⍝ Each window is a namespace with the following fields:
⍝ - id: integer identifying the window
⍝ - type : one of
⍝ 'Editor' 'Tracer' (IDE windows)
⍝ 'Notification' 'Html' (message boxes - no reply)
⍝ 'Options' 'Task' 'String' (dialog boxes - reply expected)
⍝ - title: name being edited/traced or title of dialog box
⍝ - text: content of the window (list of strings)
⍝ - line (Editor and Tracer only): current line number
⍝ - stop (Editor and Tracer only): list of line numbers that have break points ⍝ stop
⍝ - saved (Editor and Tracer only): integer specifying the SaveChanges error number, or ⍬ if edit was not saved
⍝ - options (Options and Task only): list of clickable options (list of strings)
⍝ - index (Options and task only): list of index value for each option (list of integers)
⍝ - value (String only): initial value of the text field
⍝ - default (String only): default value of the text field
⍝
⍝ errors is a 4-element vector akin to ⎕DMX fields
⍝ - EN: error number (scalar integer)
⍝ - ENX: extended error number (scalar integer)
⍝ - EM: error message (string)
⍝ - Message: detailed message (string)
⍝ Public API:
⍝
⍝ The following function execute simple APL code that must not pop-up windows (e.g. ⎕ED, 3500⌶) nor produce a non-standard prompt (⎕, ⍞, ∇-editor)
⍝ It will ⎕SIGNAL any error.
⍝ output ← APL expr ⍝ Execute a simple APL expression and get session output
⍝ The following functions execute code: they take a 3-element <wait> argument and return a 4-element <result>
⍝ <result> ← {<wait>} Execute expr ⍝ Execute an arbitrary session expression
⍝ <result> ← {<wait>} Trace expr ⍝ Start tracing an expression
⍝ <result> ← {<wait>} TraceRun win ⍝ Run current line and move to next line (step over)
⍝ <result> ← {<wait>} TraceInto win ⍝ Run current line and trace into callees (step into)
⍝ <result> ← {<wait>} TraceResume win ⍝ Resume execution of current thread
⍝ <result> ← {<wait>} TraceReturn win ⍝ Run current function until return to next line of caller
⍝ <result> ← Resume <wait> ⍝ Resume all threads
⍝ <result> ← Wait <wait> ⍝ Just wait for something to happen, without sending any message
⍝
⍝ The following functions change the state of the tracer without executing code (and don't return anything)
⍝ TraceCutback win ⍝ Cut back to caller
⍝ TraceNext win ⍝ Jump to next line
⍝ TracePrev win ⍝ Jump to previous line
⍝
⍝ src←Reformat src ⍝ Reformat code
⍝ win←EditOpen name ⍝ Start editing a name (may create a new window or jump to an existing one)
⍝ res←win EditFix src {stops} ⍝ Fix new source (and optional stops) in given window - may succeed, fail, or pop-up a dialog window
⍝ win←{type}ED name ⍝ Cover for ⎕ED that returns the created windows
⍝ win SetStops stops ⍝ Change stop points (edit or trace window)
⍝ (traces stops monitors) ← ClearTraceStopMonitor ⍝ Clear all races, stops, and monitors in the active workspace. The reply says how many of each thing were cleared.
⍝ {type} Edit (name src {stops}) ⍝ Open a name with the editor, fix the new source, and close the window. Will force overwriting the file if the interpreter asks.
⍝
⍝ wins←Windows ⍝ List all open windows
⍝ CloseWindow win ⍝ Close a window
⍝ CloseWindows ⍝ Close all windows (including message and dialog boxes)
⍝ CloseAllWindows ⍝ Close all edit/tracer windows with special RIDE protocol message
⍝ {response} Reply win ⍝ Reply to a dialog window - may resume execution in which case a Wait might be necessary
⍝ Notes:
⍝ - Edit/trace windows are represented as namespaces with fields for window attributes
⍝ (see the WINS field of this class)
⍝ Not supported:
⍝ - Multi-threading, Interrupts, Auto-completion, Value Tips
⍝ - SIStack, Threads, Status, Workspace Explorer, Process Manager
:Field Public Shared ReadOnly Version←'1.3.11'
⍝ v1.3.11 - Nic 2021
⍝ - force reading the whole buffer on TCP error
⍝ v1.3.10 - Nic 2021
⍝ - changed editor task question in v18.1
⍝ v1.3.9 - Nic 2021
⍝ - Added support to fix to mantis 18408 in v18.1 (no trace and monitor points)
⍝ v1.3.8 - Nic 2021
⍝ - Added MULTITHREADING flag to ignore spurious SetPrompt(1) from threads displaying stuff
⍝ - Added Output function to get pending output
⍝ v1.3.7 - Nic 2020
⍝ - fixed the 1400⌶ issue that was preventing garbage collection
⍝ v1.3.6 - Nic 2020
⍝ - force Conga to load into GhostRider class parent rather than in # so that )clear works when GhostRider resides in ⎕SE
⍝ v1.3.5 - Nic 2020
⍝ - added support for Edit to cope with TaskDialog (asking to load from file) before opening editor window
⍝ v1.3.4 - Nic 2020
⍝ - added APL method for simple APL evaluation (no prompt, no window)
⍝ - added Edit method for simple Editor fixing (forcing save if dialog window appears)
⍝ v1.3.3 - Nic 2020
⍝ - changed default wait to (waitprompts waitwindows←(1 2 3 4) 0) because windows pop up before the prompt comes back
⍝ v1.3.2 - Nic 2020
⍝ - avoid waiting the whole TIMEOUT in constructor
⍝ - fixed QA, added bug repros
⍝ v1.3.1 - Nic 2020
⍝ - avoid waiting the whole TIMEOUT for messages, which is slow AND unreliable
⍝ v1.3.0 - Nic 2020
⍝ - API to control dialog boxes
⍝ v1.2.0 - Nic 2020
⍝ - API to control tracer
⍝ - Added GetTcpPort to avoid re-using the same port number and failing the constructor
⍝ v1.1.0 - Nic 2020
⍝ - API to control editor
⍝ v1.0.1 - Nic 2020
⍝ - Using Tool.New to initialise Conga
⍝ - Using APLProcess to launch interpreter
⍝ - Unicode edition only
⍝ v1.0.0 - Unknown author, unknown date
⎕IO←⎕ML←1
:Field Public INFO←0 ⍝ set to 1 to log debug information
:Field Public TRACE←0 ⍝ set to 1 to fully trace the RIDE protocol
:Field Public DEBUG←0 ⍝ set to 1 to maximise the likelihood of finding a bug
:Field Public Shared ReadOnly IS181←18.1≤1 .1+.×2↑⊃(//)'.'⎕VFI 2⊃'.'⎕WG'AplVersion'
:Field Public MULTITHREADING←IS181 ⍝ allow other threads to spuriously SetPrompt(1) - seems unavoidable since Dyalog v18.1
:Field Public TIMEOUT←200 ⍝ maximum Conga timeout in milliseconds for responses that don't require significant computation
:Field Public BUFSIZE←2*15 4 ⍝ Conga buffer size for DEBUG=0 and DEBUG=1 - use small value for harsh QA that may miss messages
:Field Private Shared ReadOnly LF←⎕UCS 10
:Field Private Shared ReadOnly CR←⎕UCS 13
:Field Public NL←CR ⍝ newline character for output : (CR) is APL-friendly, (LF) is system-friendly
:Field Private Shared ReadOnly ERRNO←309 ⍝ error number signaled by this class
:Field Private Shared ReadOnly CONGA_ERRNO←999 ⍝ error number signaled by Conga
:Field Private BUFFER←0⍴⊂'' ⍝ list of received chunks
:Field Public PROCESS←⎕NULL ⍝ APLProcess to launch RIDE (if required)
:field Public CLIENT←⎕NULL ⍝ Conga connection
:Field Public Shared MyDRC←⎕NULL ⍝ Conga namespace - loaded from conga workspace - must not be called Conga nor DRC because we load Conga into this class and Tool.(New→Prepare→LoadConga) will test where.⎕NC'Conga' 'DRC' which return ¯2.2 if DRC is a Shared Field, even though it is unassigned.
:Field Public Shared APLProcess←⎕NULL ⍝ APLProcess namespace - loaded from APLProcess.dyalog
:Field Public Shared ReadOnly ERROR_OK←0 0 '' '' ⍝ error←(EN ENX EM Message)
:Field Public Shared ReadOnly ERROR_STOP←1001 0 '' '' ⍝ error returned when hitting a breakpoint
:Field Public Shared ReadOnly NO_ERROR←0⍴⊂ERROR_OK ⍝ no error produces this list of errors
:Field Public Shared ReadOnly ERROR_BREAK←,⊂ERROR_STOP ⍝ simple breakpoint produces this list of errors
:Field Public Shared ReadOnly NO_WIN←0⍴⎕NULL ⍝ empty list of windows (force prototype to ⎕NULL)
:Field Private WINS←NO_WIN ⍝ list of editor/tracer windows currently opened
Resignal←⎕SIGNAL∘{⍵/⊂⎕DMX.(('EN'EN)('ENX' ENX)('EM'EM)('Message'Message))}
Signal←⎕SIGNAL∘{(en enx em msg)←⍵ ⋄ ⊂('EN' en)('ENX' enx)('EM' em)('Message'msg)}
Error←{IsInteger ⍺: ((⍺+2)⊃⎕SI)∇⍵ ⋄ ((⍕⎕THIS),' ',⍺,' failed: ',⍕⍵)⎕SIGNAL ERRNO}
Log←{⎕←(⍕⎕THIS),' ',⍺,': ',,⍕⍵ ⋄ 1:_←⍵}
LogWarn←{⍺←'' ⋄ 1:_←('Warning',(~0∊⍴⍺)/' ',⍺)Log ⍵} ⍝ always warn
LogInfo←{⍺←'' ⋄ INFO:_←('Info',(~0∊⍴⍺)/' ',⍺)Log ⍵ ⋄ 1:_←⍵}
TrimReplyGetLog←{pre←'["ReplyGetLog",{"result":[' ⋄ post←']}]' ⋄ (pre,post)≡((≢pre)↑⍵),((-≢post)↑⍵):pre,'...',post ⋄ ⍵} ⍝ this one is too large to trace
LogTrace←{TRACE:_←⍵⊣('Trace ',⍺)Log TrimReplyGetLog ⍵ ⋄ 1:_←⍵}
GetLength←{256⊥⎕UCS 4↑⍵}
AddHeader←{((⎕UCS (4/256)⊤8+≢⍵),'RIDE'),⍵}
ToUtf8←{⎕UCS 'UTF-8'⎕UCS ⍵}
FromUtf8←{'UTF-8'⎕UCS ⎕UCS ⍵}
Stringify←{'''',((1+⍵='''')/⍵),''''}
IsStops←{(1≡≢⍴⍵)∧(1≡≡⍵)∧(⍬≡0⍴⍵)}
IsPrompts←{1≡∧/⍵∊(~⍺)↓¯1 0 1 2 3 4 5} ⍝ ⍺←1 to allow ¯1 (prompt unset)
PromptIs←{⍺∊⍵,MULTITHREADING/1}
IsSource←{(1≡≢⍴⍵)∧(2≡≡⍵)∧(∧/''∘≡¨0⍴¨⍵)}
IsWin←{⍵∊WINS}
IsString←{(1≡≢⍴⍵)∧(1≡≡⍵)∧(''≡0⍴⍵)}
IsInteger←{(0=≡⍵)∧(⍬≡0⍴⍵):⍵≡⌊⍵ ⋄ 0}
∇ ok←LoadLibraries;Tool;where
⍝ Failure to load library will cause ⎕SE.SALT.Load to error
:Access Shared
:If MyDRC≡⎕NULL
Tool←⎕SE.SALT.Load'Tool'
⍝ where←# ⍝ will prevent )clear if GhostRider resides in ⎕SE
⍝ where←⎕THIS ⍝ can't load here because refix produces a DLL ALREADY LOADED error
where←(⊃⊃⎕CLASS ⎕THIS) ⍝ load in the GhostRider class
⍝where←(⊃⊃⎕CLASS ⎕THIS).## ⍝ use the parent of the class to avoid reinitialising DRC on refix
MyDRC←Tool.New'Conga' '' ''(0 0 0)where
{}MyDRC.SetProp'' 'EventMode' 1
:EndIf
:If APLProcess≡⎕NULL
APLProcess←⎕SE.SALT.Load'APLProcess -target=',⍕⊃⊃⎕CLASS ⎕THIS ⍝ ensure we load it in the shared class and not in the instance
:EndIf
∇
∇ port←GetTcpPort;addr;rc;srv
⍝ find a free TCP port by starting and closing a conga server (pretty heavy weight...)
:Access Public
(rc srv)←MyDRC.Srv'' '127.0.0.1' 0 'Text'
:If rc≠0 ⋄ 'GetTcpPort'Error'Failed to start server' ⋄ :EndIf
(rc addr)←MyDRC.GetProp srv'LocalAddr'
:If rc≠0 ⋄ 'GetTcpPort'Error'Failed to get local TCP/IP address' ⋄ :EndIf
port←4⊃addr
CloseConga srv
∇
∇ Constructor0
:Access Public
:Implements Constructor
Constructor ⍬
∇
∇ Constructor args;RIDE_INIT;_;env;host;port;r;runtime;tm1;tm2
:Access Public
:Implements Constructor
⎕RL←⍬ 1 ⍝ for ED and _RunQA
LoadLibraries
:If (0∊⍴args)∨(''≡0⍴args) ⍝ spawn a local Ride - args is {env}
host←'127.0.0.1' ⋄ port←GetTcpPort
env←,⍕args ⋄ RIDE_INIT←'serve::',⍕port ⍝ only accept local connections
runtime←0 ⍝ otherwise we'd need to keep the interpreter busy
:If 0∊⍴('\sDYAPP='⎕S 0)env ⍝ don't inherit some environment variables
env,←' DYAPP='
:EndIf
⍝:If 0∊⍴('\sSESSION_FILE='⎕S 0)env ⍝ don't inherit some environment variables
⍝ env,←' SESSION_FILE='
⍝:EndIf
PROCESS←⎕NEW APLProcess(''env runtime RIDE_INIT)
⎕DL 0.3 ⍝ ensure process doesn't exit early
:If PROCESS.HasExited
'Constructor'Error'Failed to start APLProcess: RIDE_INIT=',RIDE_INIT,' env=',env
:EndIf
:Else ⍝ connect to an existing Ride - args is (port {host})
(port host)←2↑args,⊂'' ⍝ port is integer and must be specified
:If ⍬≢⍴port ⋄ :OrIf ⍬≢0⍴port ⋄ :OrIf port≠⌊port ⋄ :OrIf 0≠11○port ⋄ :OrIf port≤0
'Constructor'Error'Port number must be positive integer: ',⍕port
:EndIf
:If 0∊⍴host ⋄ host←'127.0.0.1' ⋄ :EndIf ⍝ default to local machine
args←''
PROCESS←⎕NULL ⍝ no process started
:EndIf
tm1←'SupportedProtocols=2' ⋄ tm2←'UsingProtocol=2'
⎕DF('@',host,':',⍕port){(¯1↓⍵),⍺,(¯1↑⍵)}⍕⎕THIS
:If 0≠⊃(_ CLIENT)←2↑r←MyDRC.Clt''host port'Text'((1+DEBUG)⊃BUFSIZE)
'Constructor'Error'Could not connect to server ',host,':',⍕port
:ElseIf 1=≢('Constructor'WaitFor 0)tm1 ⍝ first message is not JSON
:AndIf Send tm1
:AndIf 1=≢('Constructor'WaitFor 0)tm2 ⍝ second message is not JSON
:AndIf Send tm2
:AndIf EmptyQueue'Identify'
⍝ from this point on, Windows interpreter may or may not send zero or more FocusThread or SetPromptType at different points in time
:AndIf Send'["Identify",{"identity":1}]'
:AndIf 2≤≢'FocusThread' 'SetPromptType'('Constructor'WaitFor 1)'Identify' 'UpdateDisplayName' ⍝ interpreter sends one 'Identify' and one 'UpdateDisplayName'
:AndIf Send'["Connect",{"remoteId":2}]'
:AndIf 1≤≢'FocusThread' 'ReplyGetLog'('Constructor'WaitFor 1)'SetPromptType' ⍝ interpreter sends zero or more 'ReplyGetLog' and one 'SetPromptType'
:AndIf Send'["CanSessionAcceptInput",{}]'
:AndIf 1≤≢'FocusThread' 'SetPromptType'('Constructor'WaitFor 1)'CanAcceptInput'
:AndIf Send'["GetWindowLayout",{}]' ⍝ what's this for ????
:AndIf EmptyQueue'GetWindowLayout' ⍝ no response to GetWindowLayout ????
:AndIf Send'["SetPW",{"pw":32767}]'
:AndIf EmptyQueue'SetPW'
⍝:AndIf 1('⍫⌽⍋⍒<¯¨',NL)(NO_WIN)(NO_ERROR)≡Execute'⌽''¨¯<⍒⍋⍉⍫''' ⍝ try and execute APL
LogInfo'Connection established'
:Else
Terminate
'Constructor'Error'RIDE handshake failed'
:EndIf
∇
∇ CloseConga obj
:Trap CONGA_ERRNO ⍝ )clear can un-initialise Conga, making MyDRC.Close ⎕SIGNAL 999 instead of returning error code 1006 - ERR_ROOT_NOT_FOUND - Please re-initialise
{}MyDRC.Close obj
:EndTrap
∇
∇ Terminate
:Implements Destructor
:If PROCESS≢⎕NULL ⍝ we did spawn an interpreter
:AndIf ~PROCESS.HasExited ⍝ APLProcess destructor generally triggers before this one
{}LogInfo'Shutting down spawned interpreter'
:Trap CONGA_ERRNO ⍝ Conga may throw error 1006 - ERR_ROOT_NOT_FOUND - Please re-initialise
{}0 Send'["Exit",{"code":0}]' ⍝ attempt to shut down cleanly - APLProcess will kill it anyways
:EndTrap
:EndIf
:If CLIENT≢⎕NULL
{}LogInfo'Closing connection'
CloseConga CLIENT
:EndIf
CLIENT←⎕NULL
PROCESS←⎕NULL
∇
∇ terminated←Terminated
terminated←CLIENT≡⎕NULL
∇
∇ {ok}←{error}Send msg;r
⍝ Send a message to the RIDE
:Access Public
:If 0=⎕NC'error' ⋄ error←1 ⋄ :EndIf
ok←0=⊃r←MyDRC.Send CLIENT(AddHeader ToUtf8'Send'LogTrace msg)
:If error∧~ok
'Send'Error⍕r
Terminate
:EndIf
∇
∇ messages←{timeout}Read json;buffer;bufok;done;len;ok;r;start
⍝ Read message queue from the RIDE
:Access Public
:If 0=⎕NC'timeout' ⋄ timeout←TIMEOUT ⋄ :EndIf
:Repeat
:If ok←0=⊃r←MyDRC.Wait CLIENT timeout
:If r[3]∊'Block' 'BlockLast' ⍝ we got some data
BUFFER,←⊂4⊃r
:EndIf
done←r[3]∊'BlockLast' 'Closed' 'Timeout' ⍝ only a timeout is normal behaviour because RIDE connection is never closed in normal operation
ok←~r[3]∊'BlockLast' 'Closed' ⍝ interpreter closed connection (should not happen)
:Else ⋄ done←1 ⍝ ok←0
:EndIf
:Until done
messages←0⍴⊂''
buffer←∊BUFFER ⋄ bufok←1
:While (len←GetLength buffer)≤≢buffer
:AndIf bufok∧←('RIDE'≡4↓8↑buffer)∧(8<len)
messages,←⊂0 ⎕JSON⍣json⊢'Receive'LogTrace FromUtf8 8↓len↑buffer
buffer←len↓buffer
:EndWhile
:If bufok ⋄ BUFFER←,⊂buffer
:Else ⋄ BUFFER←0⍴⊂'' ⋄ 'Read'Error'Invalid buffer: ',buffer
:EndIf
:If ~ok∧bufok
'Read'LogWarn'Connection failed: ',⍕r
Terminate ⍝ consider connection dead for good (avoid trying to read more)
:EndIf
∇
∇ {messages}←{ignored}(fn WaitFor json)awaited;commands;messages;timeout
⍝ simple waiting of messages, without grabbing any result
:If 0=⎕NC'ignored' ⋄ ignored←'' ⋄ :EndIf
:If 0∊⍴ignored ⋄ ignored←0⍴⊂'' ⋄ :Else ⋄ ignored←,⊆,ignored ⋄ :EndIf
awaited←,⊆,awaited ⋄ messages←commands←⍬ ⋄ timeout←0
:Repeat
:If ~DEBUG ⋄ timeout←TIMEOUT⌊timeout+10 ⋄ :EndIf
commands,←⊃¨⍣json⊢messages,←timeout Read json ⍝ if json, just care about the message header
:If ~0∊⍴commands~awaited∪ignored ⍝ unexpected messages
fn Error'Received unexpected messages: ',⍕commands~awaited∪ignored
:EndIf
:Until Terminated∨(∧/awaited∊commands)
∇
∇ {ok}←{timeout}EmptyQueue msg;messages
⍝ empty message queue, expecting it to be empty
:If 0=⎕NC'timeout' ⋄ timeout←0 ⋄ :EndIf ⍝ do not wait for new messages
messages←timeout Read 1
:If ~ok←0∊⍴messages ⋄ msg LogInfo'Message queue not empty: ',⍕⊃¨messages ⋄ :EndIf
∇
∇ wins←Windows
:Access Public
wins←WINS
∇
∇ win←new GetWindow id;inx
:If (~0∊⍴WINS) ⋄ :AndIf (≢WINS)≥(inx←WINS.id⍳id) ⋄ win←inx⊃WINS ⍝ found
:ElseIf new ⋄ win←⎕NS ⍬ ⋄ win.id←id ⋄ WINS,←win ⍝ create new window
:Else ⋄ win←NO_WIN ⍝ not found
:EndIf
∇
∇ id←NextId
⍝ Generate a local id that cannot be submitted by the interpreter (negative)
:If 0∊⍴WINS ⋄ id←¯1
:Else ⋄ id←¯1+⌊/0,WINS.id
:EndIf
∇
∇ (prompt output wins errors)←fn ProcessMessages messages;arguments;command;em;en;enx;msg;win
prompt←¯1 ⍝ ¯1 = prompt unset, 0 = no prompt, 1 = the usual 6-space APL prompt (a.k.a. Descalc or "desktop calculator"), 2 = Quad(⎕) input, 3 = ∇ line editor, 4 = Quote-Quad(⍞) input, 5 = any prompt type unforeseen here.
output←'' ⍝ session output (string with newlines)
wins←NO_WIN ⍝ saved/modified windows (list of namespaces from WINS)
errors←NO_ERROR ⍝ list of ⎕DMX.(EN EM Message)
:For command arguments :In messages
win←NO_WIN
:Select command
:CaseList 'CanAcceptInput' 'FocusThread' 'EchoInput' 'UpdateDisplayName' 'StatusOutput' ⍝ these are ignored
:Case 'SetPromptType'
prompt←arguments.type
:Case 'AppendSessionOutput'
output,←⊂{LF=⊃⌽⍵:NL@(≢⍵)⊢⍵ ⋄ ⍵}arguments.result
:Case 'HadError' ⍝ ⎕SIGNAL within APL execution
errors,←⊂(en enx em msg)←arguments.(error dmx'' '') ⍝ arguments.dmx gives ⎕DMX.ENX
:Case 'InternalError' ⍝ Error within RIDE message processing
errors,←⊂(en enx em msg)←arguments.(error error_text dmx) ⍝ arguments.message gives the failing RIDE command
:CaseList 'OpenWindow' 'UpdateWindow'
win←1 GetWindow arguments.token
:If 0=arguments.⎕NC'trace' ⋄ arguments.trace←⍬ ⋄ :EndIf ⍝ no trace points up to v18.1
:If 0=arguments.⎕NC'monitor' ⋄ arguments.monitor←⍬ ⋄ :EndIf ⍝ no monitor points up to v18.1
win.(title text line stop trace monitor saved)←arguments.(name text currentRow stop trace monitor ⍬)
win.type←(1+arguments.debugger)⊃'Editor' 'Tracer'
:Case 'GotoWindow'
:If 0∊⍴win←0 GetWindow arguments.win
'GotoWindow'Error'Unknown window: ',⍕arguments.win
:EndIf
:Case 'WindowTypeChanged'
:If 0∊⍴win←0 GetWindow arguments.win
'WindowTypeChanged'Error'Unknown window: ',⍕arguments.win
:EndIf
win.type←(1+arguments.tracer)⊃'Editor' 'Tracer'
:Case 'SetHighlightLine'
:If 0∊⍴win←0 GetWindow arguments.win
'SetHighlightLine'Error'Unknown window: ',⍕arguments.win
:EndIf
win.line←arguments.line
:Case 'SetLineAttributes'
:If 0∊⍴win←0 GetWindow arguments.win
'SetLineAttributes'Error'Unknown window: ',⍕arguments.win
:EndIf
win.stop←arguments.stop
:Case 'ReplySaveChanges'
:If 0∊⍴win←0 GetWindow arguments.win
'ReplySaveChanges'Error'Unknown window: ',⍕arguments.win
:EndIf
win.saved←arguments.err
:Case 'ReplyFormatCode'
:If 0∊⍴win←0 GetWindow arguments.win
'ReplyFormatCode'Error'Unknown window: ',⍕arguments.win
:EndIf
win.(text saved)←arguments.(text 0)
:Case 'CloseWindow'
:If 0∊⍴win←0 GetWindow arguments.win
'CloseWindow'Error'Unknown window: ',⍕arguments.win
:EndIf
RemoveWindows win
:Case 'NotificationMessage'
win←1 GetWindow NextId
win.(title type text)←'' 'Notification'(,⊂arguments.message)
:Case 'ShowHTML'
win←1 GetWindow NextId
win.(title type text)←'' 'Html'(,⊂arguments.html)
:Case 'OptionsDialog'
win←1 GetWindow arguments.token
win.(title type text options index)←arguments.(title'Options'text options(¯1+⍳≢options))
:Case 'TaskDialog'
win←1 GetWindow arguments.token
win.(title type text options index)←arguments.(title'Task'text(options,buttonText)((¯1+⍳≢options),(99+⍳≢buttonText)))
:Case 'StringDialog'
win←1 GetWindow arguments.token
win.(title type text value default)←arguments.(title'String'text initialValue defaultValue)
:Case 'SysError'
LogInfo'SysError: ',⍕arguments.(text stack)
Terminate
done←1
fn Error'Interpreter system error',arguments.(text,': ',stack)
:Case 'Disconnect'
LogInfo'Disconnected: ',arguments.message
Terminate
done←1
fn Error'Interpreter unexpectedly disconnected'
:Else
fn LogWarn'Unexpected RIDE command: ',command
:EndSelect
wins,←win
:EndFor
output←⊃,/output
∇
∇ (prompt output wins errors)←{wait}(fn WaitSub)waitmessages;done;messages;nothing;numwins;timeout;waitmessages;waitprompts;waitwins
⍝ Process incoming messages until all wait conditions are fulfilled
⍝ Exceptions when waiting no prompt and no window : the first event that happens returns
:If 0=⎕NC'wait' ⋄ :OrIf 0∊⍴wait ⋄ (waitprompts waitwins)←(1 2 3 4)0 ⍝ wait for prompt to come back by default - windows generally pop up before that
:Else ⋄ (waitprompts waitwins)←wait ⍝ wait for both conditions
:EndIf
⍝ waitmessages is a depth-3 list of RIDE protocol messages, at least one of the lists must have all its messages received
:If 0∊⍴waitmessages ⋄ waitmessages←0⍴⊂0⍴⊂''
:Else ⋄ waitmessages←,¨¨{,⊆,⍵}¨{⊂⍣(2≥|≡⍵)⊢⍵}{,⊆,⍵}waitmessages
:EndIf
:If 2>|≡waitmessages ⋄ waitmessages←{,⊆,⍵}¨waitmessages ⋄ :EndIf
:If 3>|≡waitmessages ⋄ waitmessages←,⊂waitmessages ⋄ :EndIf
prompt←⍬ ⋄ output←'' ⋄ wins←NO_WIN ⋄ errors←NO_ERROR ⋄ numwins←0 ⋄ timeout←0 ⋄ messages←0⍴⊂''
:If ~0 IsPrompts waitprompts←,waitprompts ⋄ fn Error'Invalid awaited prompt types: ',⍕waitprompts
:ElseIf IsInteger waitwins ⋄ :AndIf waitwins≥0 ⋄ numwins←1 ⍝ target number of windows
:ElseIf 0∊⍴waitwins←,waitwins ⋄ :OrIf ∧/IsWin¨waitwins ⋄ numwins←0 ⍝ list of windows to touch
:Else ⋄ fn Error'Invalid awaited windows: ',⍕waitwins
:EndIf
:If numwins ⋄ nothing←waitwins=0 ⋄ :Else ⋄ nothing←0∊⍴waitwins ⋄ :EndIf
nothing∧←0∊⍴waitprompts
:Repeat
:If ~DEBUG ⋄ timeout←TIMEOUT⌊timeout+10 ⋄ :EndIf ⍝ crank up timeout 10 ms at a time to avoid consuming too much CPU
(prompt output wins errors),←fn ProcessMessages messages,←timeout Read 1
:If nothing ⋄ done←(∨/~prompt∊¯1 0)∨(~0∊⍴wins)
:Else
:If numwins ⋄ done←waitwins≤≢wins ⋄ :Else ⋄ done←∧/waitwins∊wins ⋄ :EndIf
done∧←(0∊⍴waitprompts)∨(∨/prompt∊waitprompts)
:EndIf
done∧←(0∊⍴waitmessages)∨(∨/∧/¨waitmessages∊¨⊂⊃¨messages)
:Until Terminated∨done
prompt←⊃⌽¯1,prompt~¯1 ⍝ only the last prompt set is interesting
wins←∪wins ⍝ window may get several messages e.g. UpdateWindow+SetHighlightLine
:If ~0∊⍴errors ⋄ errors←,⊂GetError ⋄ :EndIf
∇
∇ (prompt output wins errors)←Wait wait
⍝ Get last prompt type, session output, touched windows and thrown errors
⍝ wait may be empty or specify (waitprompts waitwins)
:Access Public
(prompt output wins errors)←wait('Wait'WaitSub)⍬
∇
∇ result←Resume wait
⍝ Resume execution of all threads
:Access Public
Send'["RestartThreads",{}]'
result←wait('Resume'WaitSub)⍬
∇
∇ result←{wait}Execute expr;wins
⍝ Execute an APL expression.
:Access Public
:If 0=⎕NC'wait' ⋄ wait←⊢ ⋄ :EndIf
:If ~IsString expr←,expr ⋄ 'Execute'Error'Expression must be a string' ⋄ :EndIf
EmptyQueue'Execute'
Send'["Execute",{"text":',(1 ⎕JSON expr,LF),',"trace":0}]' ⍝ DOC error : trace is 0|1 not true|false
result←wait('Execute'WaitSub)'EchoInput'
∇
∇ result←{wait}Trace expr;wins
⍝ Trace into an APL expression.
:Access Public
:If 0=⎕NC'wait' ⋄ wait←⊢ ⋄ :EndIf
:If ~IsString expr←,expr ⋄ 'Trace'Error'Expression must be a string' ⋄ :EndIf
EmptyQueue'Trace'
Send'["Execute",{"text":',(1 ⎕JSON expr,LF),',"trace":1}]' ⍝ DOC error : trace is 0|1 not true|false
result←wait('Trace'WaitSub)'EchoInput'
∇
∇ result←{wait}TraceRun win
⍝ Run current line and move to next (step over)
:Access Public
:If 0=⎕NC'wait' ⋄ wait←⊢ ⋄ :EndIf
Send'["RunCurrentLine",{"win":',(1 ⎕JSON win.id),'}]'
result←wait('TraceRun'WaitSub)⍬
∇
∇ result←{wait}TraceInto win
⍝ Run current line and trace callees (step into)
:Access Public
:If 0=⎕NC'wait' ⋄ wait←⊢ ⋄ :EndIf
Send'["StepInto",{"win":',(1 ⎕JSON win.id),'}]'
result←wait('TraceInto'WaitSub)⍬
∇
∇ result←{wait}TraceResume win
⍝ Resume execution of current thread
:Access Public
:If 0=⎕NC'wait' ⋄ wait←⊢ ⋄ :EndIf
Send'["Continue",{"win":',(1 ⎕JSON win.id),'}]'
result←wait('TraceResume'WaitSub)⍬
∇
∇ result←{wait}TraceReturn win
⍝ Run current function until return to next line of caller
:Access Public
:If 0=⎕NC'wait' ⋄ wait←⊢ ⋄ :EndIf
Send'["ContinueTrace",{"win":',(1 ⎕JSON win.id),'}]'
result←wait('TraceReturn'WaitSub)⍬
∇
∇ output←APL expr;errors;prompt;wins
⍝ Execution of a simple APL expression and get session output
:Access Public
(prompt output wins errors)←1 0 Execute expr ⍝ wait for prompt=1 and no window
:If ~prompt PromptIs 1 ⋄ 'APL'Error'Expression produced non-standard prompt: ',expr
:ElseIf wins≢NO_WIN ⋄ 'APL'Error'Expression produced a window: ',expr
:ElseIf errors≢NO_ERROR ⋄ Signal⊃errors
:EndIf
∇
∇ output←Output;errors;prompt;wins
⍝ Read pending output to session - no wait
:Access Public
(prompt output wins errors)←'Output'ProcessMessages 0 Read 1
:If ~prompt PromptIs ¯1 ⋄ 'Output'Error'Interpreter changed prompt'
:ElseIf wins≢NO_WIN ⋄ 'Output'Error'Interpreter produced a window'
:ElseIf errors≢NO_ERROR ⋄ Signal⊃errors
:EndIf
∇
∇ num←fn VFI txt;ok
⍝ convert a single number
(ok num)←⎕VFI txt
:If (ok≡,1) ⋄ num←⊃num
:Else ⋄ fn Error'Could not parse number: ',txt
:EndIf
∇
∇ (en enx em msg)←GetError;r
⍝ Get last APL error information
:Access Public
r←Execute¨'⎕DMX.EN' '⎕DMX.ENX' '⎕DMX.EM' '⎕DMX.Message' ⍝ would infinitely loop on error
:If (1∨.≠1⊃¨r) ⋄ :OrIf (NL∨.≠{⊃⌽⍵}¨2⊃¨r) ⋄ :OrIf 0∨.<⊃∘⍴¨3⊃¨r ⋄ :OrIf 0∨.<⊃∘⍴¨4⊃¨r
'GetError'Error'Failed to retrieve ⎕DMX information'
:EndIf
(en enx em msg)←¯1↓¨2⊃¨r ⍝ keep output only
(en enx)←'GetError'∘VFI¨en enx
∇
∇ {response}Reply win;result
⍝ response must be one of win.options or win.index for Options and Task dialogs
⍝ response must be a string for String dialogs
⍝ No response means close the window
⍝ Because replying to a dialog window will resume execution, the result is a full execution result
:Access Public
:Select win.type
:CaseList 'Options' 'Task'
:If 0=⎕NC'response' ⋄ response←¯1 ⍝ just close the window
:ElseIf (⊂response)∊win.options ⋄ response←(win.options⍳⊂response)⊃win.index
:ElseIf (⊂response)∊win.index ⍝ response is ok
:Else ⋄ 'Reply'Error'Invalid response for a ',win.type,' dialog'
:EndIf
Send'["Reply',win.type,'Dialog",{"index":',(1 ⎕JSON response),',"token":',(1 ⎕JSON win.id),'}]' ⍝ DOC index is a string ???
:Case 'String'
:If 0=⎕NC'reponse' ⋄ response←win.value ⋄ :EndIf ⍝ edit field untouched
Send'["Reply',win.type,'Dialog",{"value":',(1 ⎕JSON response),',"token":',(1 ⎕JSON win.id),'}]'
:EndSelect
RemoveWindows win
∇
∇ {list}←{list}RemoveWindows wins;default
⍝ ensure prototype of empty list (changing WINS by default)
:If default←0=⎕NC'list' ⋄ list←WINS ⋄ :EndIf
list~←wins ⋄ :If 0∊⍴list ⋄ list←NO_WIN ⋄ :EndIf
:If default ⋄ WINS←list ⋄ :EndIf
∇
∇ CloseWindow win;done;errors;line;message;messages;ok;output;prompt;wins
⍝ Close an edit or tracer window
:Access Public
:If ~IsWin win ⋄ 'CloseWindow'Error'Argument must be a window' ⋄ :EndIf
:Select win.type
:CaseList 'Editor' 'Tracer'
Send message←'["CloseWindow",{"win":',(1 ⎕JSON win.id),'}]'
:Repeat
(prompt output wins errors)←⍬ win('CloseWindow'WaitSub)('CloseWindow')('WindowTypeChanged' 'SetHighlightLine') ⍝ turning editor into tracer requires two messages : WindowTypeChanged, SetHighlightLine
ok←(prompt PromptIs ¯1)∧(output≡'')∧(errors≡NO_ERROR)∧(wins≡,⊂win)
:If ok∧(~IsWin win) ⋄ done←1 ⍝ actually closed the window
:ElseIf ok∧(IsWin win) ⋄ :AndIf 'Tracer'≡win.type ⋄ done←1 ⍝ edit turned back into a tracer
:Else ⋄ done←win∊wins ⍝
:EndIf
:Until Terminated∨done
:If ~ok ⋄ 'CloseWindow'Error'Failed to close window ',⍕win.id ⋄ :EndIf
:CaseList 'Options' 'Task' 'String'
Reply win
:CaseList 'Notification' 'Html'
RemoveWindows win ⍝ message boxes are discarded without response
:Else ⋄ 'CloseWindow'Error'Invalid window type'
:EndSelect
∇
∇ CloseWindows
⍝ Close all existing windows
:Access Public
:If ~0∊⍴WINS ⋄ CloseWindow¨WINS ⋄ :EndIf
∇
∇ CloseAllWindows;errors;messages;output;prompt;toclose;wins
⍝ Close all edit/tracer windows with special RIDE protocol message
:Access Public
Send'["CloseAllWindows",{}]'
:If 0∊⍴WINS ⋄ toclose←WINS
:Else ⋄ toclose←(WINS.type∊'Editor' 'Tracer')/WINS
:EndIf
:If 0∊⍴toclose ⋄ TIMEOUT EmptyQueue'CloseAllWindows' ⋄ :Return ⋄ :EndIf ⍝ ensure no response
⍝:While ~0∊⍴toclose
(prompt output wins errors)←⍬ toclose('CloseAllWindows'WaitSub)⍬
:If ~prompt PromptIs ¯1 1 ⋄ 'CloseAllWindows'Error'Produced unexpected prompt: ',⍕prompt
:ElseIf errors≢NO_ERROR ⋄ 'CloseAllWindows'Error'Produced unexpected errors: ',⍕errors
:ElseIf output≢'' ⋄ 'CloseAllWindows'Error'Produced unexpected output: ',⍕output
:ElseIf {1∨.≠(+/⍵),(+⌿⍵)}toclose∘.≡wins ⋄ 'CloseAllWindows'Error'Did not produce close all windows'
:EndIf
⍝ toclose~←wins
⍝:EndWhile
∇
∇ win←{type}ED name;errors;expr;output;prompt;types;wins
⍝ Cover for ⎕ED to open one editor window, allowing to specify its type if name is undefined
⍝ The window might be a TaskDialog asking whether we should read from file
:Access Public
:If 0=⎕NC'type' ⋄ type←''
:ElseIf ~IsString name ⋄ 'ED'Error'Right argument must be a string'
:ElseIf ¯1=⎕NC name ⋄ 'ED'Error'Invalid name: ',name ⍝ ⎕ED silently fails but we don't
:ElseIf ~type∊types←'∇→∊-⍟○∘' ⋄ 'ED'Error'Left argument must be a character amongst ',types
:Else ⋄ type←Stringify type
:EndIf
expr←type,'⎕ED',⍕Stringify name
EmptyQueue'ED'
Send'["Execute",{"text":',(1 ⎕JSON expr,LF),',"trace":false}]'
(prompt output wins errors)←⍬ 1('ED'WaitSub)⍬
:If ~0∊⍴errors ⋄ 'ED'Error'Produced unexpected errors: ',⍕errors
:ElseIf ~0∊⍴output ⋄ 'ED'Error'Produced unexpected output: ',⍕output
:ElseIf 1≠≢wins ⋄ 'ED'Error'Did not produce 1 window'
:ElseIf wins.type≡,⊂'Editor'
:If wins.title≢,⊂{(⌽∧\⌽⍵≠'.')/⍵}name ⋄ 'ED'Error'Did not edit expected name' ⋄ :EndIf
:If ~prompt PromptIs 1 ⋄ :AndIf (1 ''NO_WIN NO_ERROR)≡1 0('ED'WaitSub)⍬ ⋄ 'ED'Error'Failed to come back to prompt' ⋄ :EndIf
:ElseIf ~(wins.type)∊'Options' 'Task' ⋄ 'ED'Error'Opened something else than an Options/Task window: ',(⊃wins).type
:ElseIf ~prompt PromptIs 0 ⋄ 'ED'Error'Produced unexpected prompt: ',⍕prompt
:EndIf
win←⊃wins
∇
∇ win←EditOpen name;errors;ok;output;prompt;wins
⍝ Edit a name, get a window.
⍝ To specify the type of entity to edit, use ED instead.
:Access Public
:If ~IsString name←,name ⋄ :OrIf ¯1=⎕NC name ⋄ 'EditOpen'Error'Right argument must be a valid APL name' ⋄ :EndIf ⍝ otherwise EditOpen '' loops forever
⍝:If 0=⎕NC'type' ⋄ type←'∇' ⋄ :EndIf
⍝:If ~type∊'∇→∊-⍟○∘' ⋄ 'EditOpen'Error'Left argument must be one of: ∇: fn/op ⋄ →: string ⋄ ∊: vector of strings ⋄ -: character matrix ⋄ ⍟: namespace ⋄ ○: class ⋄ ∘: interface' ⋄ :EndIf
⍝type←1 128 16 2 256 512 1024['∇→∊-⍟○∘'⍳type] ⍝ Constants for entityType: 1 defined function, 2 simple character array, 4 simple numeric array, 8 mixed simple array, 16 nested array, 32 ⎕OR object, 64 native file, 128 simple character vector, 256 APL namespace, 512 APL class, 1024 APL interface, 2048 APL session, 4096 external function.
win←NO_WIN ⍝ failed to open anything
EmptyQueue'EditOpen'
Send'["Edit",{"win":0,"text":',(1 ⎕JSON name),',"pos":1,"unsaved":{}}]' ⍝ ⎕BUG doesn't work if unsaved not specified ? ⎕DOC : win must be 0 to create a new window
(prompt output wins errors)←⍬ 1('EditOpen'WaitSub)⍬ ⍝ Edit does not touch the prompt - wait for window to pop up
:If ~prompt PromptIs ¯1 ⋄ 'EditOpen'Error'Produced unexpected prompt: ',⍕prompt
:ElseIf NO_ERROR≢errors ⋄ 'EditOpen'Error'Produced unexpected error: ',⍕errors
:ElseIf ''≢output ⋄ 'EditOpen'Error'Produced unexpected output: ',⍕output
:ElseIf 1≠≢wins ⋄ 'EditOpen'Error'Failed to open 1 window'
:ElseIf wins.type≡,⊂'Editor'
:If wins.title≢,⊂{(⌽∧\⌽⍵≠'.')/⍵}name ⋄ 'EditOpen'Error'Did not edit expected name' ⋄ :EndIf
:ElseIf ~(wins.type)∊'Options' 'Task' ⋄ 'EditOpen'Error'Opened something else than an Options/Task window: ',(⊃wins).type
:EndIf
win←⊃wins
∇
∇ res←win EditFix src;arguments;command;errors;messages;monitor;output;prompt;stop;trace;wins
⍝ Fix source in a given edit window
⍝ result is boolean if fixing was completed (1 for OK, 0 for error)
⍝ otherwise it's an OptionsDialog popped up by the editor
:Access Public
:If 2=|≡src←,⊆,src ⋄ src←⊂src ⋄ :EndIf ⍝ source only, no stop/trace/monitor defined
(src stop trace monitor)←src,(≢src)↓4⍴⎕NULL
:If ⎕NULL≡stop ⋄ stop←win.stop ⋄ :EndIf
:If ⎕NULL≡trace ⋄ trace←win.trace ⋄ :EndIf
:If ⎕NULL≡monitor ⋄ monitor←win.monitor ⋄ :EndIf
:If ~IsStops stop ⋄ 'EditFix'Error'Stop points must be numeric vector: ',⍕stop ⋄ :EndIf
:If ~IsStops trace ⋄ 'EditFix'Error'Trace points must be numeric vector: ',⍕trace ⋄ :EndIf
:If ~IsStops monitor ⋄ 'EditFix'Error'Monitor points must be numeric vector: ',⍕monitor ⋄ :EndIf
⍝stop∩←trace∩←monitor∩←0,⍳⍴src ⍝ only legitimate line numbers
:If ~IsSource src←,¨src ⋄ 'EditFix'Error'Source must be a string or a vector of strings: ',⍕src ⋄ :EndIf
:If ~IsWin win ⋄ 'EditFix'Error'Left argument must be a window' ⋄ :EndIf
win.saved←⍬
EmptyQueue'EditFix'
Send'["SaveChanges",{"win":',(1 ⎕JSON win.id),',"text":',(1 ⎕JSON src),',"stop":',(1 ⎕JSON stop),',"trace":',(1 ⎕JSON trace),',"monitor":',(1 ⎕JSON monitor),'}]' ⍝ providing no stop is like explicitly setting stops to ⍬
(prompt output wins errors)←⍬ 1('EditFix'WaitSub)⍬ ⍝ EditFix may not touch the prompt - wait for window to be touched
:If ~prompt PromptIs ¯1 1 ⋄ 'EditFix'Error'Produced unexpected prompt: ',⍕prompt ⍝ ⎕BUG ? Even though promptype was already 1, SaveChanges may trigger two SetPromptType(1) messages before the ReplySaveChanges, and may trigger one before an OptionsDialog
:ElseIf NO_ERROR≢errors ⋄ 'EditFix'Error'Produced unexpected error: ',⍕errors
:ElseIf ''≢output ⋄ 'EditFix'Error'Produced unexpected output: ',⍕output
:ElseIf 1≠≢wins ⋄ 'EditFix'Error'Failed to fix 1 window'
:ElseIf wins≡,⊂win ⋄ res←0≡win.saved ⍝ fixed
:ElseIf ~(wins.type)∊'Options' 'Task' ⋄ 'EditFix'Error'Opened something else than an Options/Task window: ',(⊃wins).type
:Else ⋄ res←⊃wins ⍝ a wild dialog window appears
:EndIf
win.(text stop trace monitor)←src stop trace monitor
∇
∇ {type}Edit args;errors;monitor;name;ok;opt;output;prompt;res;src;stop;trace;win;wins
⍝ {type} Edit (name src {stop} {trace} {monitor})
:Access Public
⍝ args←,⊆,args ⍝ single name won't work - source is required
(name src stop trace monitor)←args,(≢args)↓''(0⍴⊂'')⎕NULL ⎕NULL ⎕NULL
:If 0=⎕NC'type' ⋄ type←⊢ ⋄ :EndIf
win←type ED name
:If IS181 ⋄ opt←'Use the contents of the modified file' ⍝ added around Dyalog 18.1.40300
:Else ⋄ opt←'Get the text from the modified file'
:EndIf
:If win.type≡'Task' ⋄ :AndIf (⊂opt)∊win.options
opt Reply win
(prompt output wins errors)←Wait 1 1 ⍝ prompt must come back to 1
:If ~prompt PromptIs 1 ⋄ :AndIf (output errors)≢''NO_ERROR ⋄ 'Edit'Error'Failed to reply to TaskDialog' ⋄ :EndIf
win←⊃wins
:EndIf
:If win.type≢'Editor' ⋄ 'Edit'Error'Failed to open editor' ⋄ :EndIf
:If ~ok←1≡res←win EditFix src stop trace monitor ⍝ 1 is OK - 0 is failure to fix - namespace is a dialog box
opt←('Fix as code in the workspace',(⎕UCS 10),'Create objects in the workspace, and update the file')
:If ok←9=⎕NC'res' ⍝ dialog box
:If res.type≡'Task' ⋄ :AndIf (⊂opt)∊res.options ⍝ ask to overwrite file
opt Reply res ⍝ say yes
win.saved←⍬
(prompt output wins errors)←Wait 1 win ⍝ wait for save changes on the editor window
ok←(prompt∊0 1)∧(''≡output)∧(0≡win.saved)∧(wins≡,win)∧(errors≡NO_ERROR)
:Else
Reply res ⋄ ok←0 ⍝ cancel
:EndIf
:EndIf
:EndIf
CloseWindow win
:If ~ok ⋄ 'Edit'Error'Could not fix name ',name,' with source: ',⍕src ⋄ :EndIf
∇
∇ src←Reformat src;arguments;command;errors;messages;ns;ok;output;prompt;script;trad;type;win;wins
⍝ Reformat source as vectors of strings
:Access Public
:If ~IsSource src←,¨,⊆,src ⋄ 'EditFix'Error'Source must be a string or a vector of strings: ',⍕src ⋄ :EndIf
ns←⎕NS ⍬ ⋄ trad←script←ok←0
⍝ BUG: Mantis 18310: on windows, the interpreter sends ReplyFormatCode with incorrect format if type is '∇' and source is that of a :Namespace, rather than send OptionsDialog like linux does. Similarly when trying to format a function with type '○'
⍝ This is why we detect the type first hand by fixing it locally
:Trap 0 ⋄ trad←IsString ns.⎕FX src ⋄ :EndTrap
:Trap 0 ⋄ {}ns.⎕FIX src ⋄ script←1 ⋄ :EndTrap
:For type :In script trad/'○∇' ⍝ Either a function or a script
win←type ED ⎕A[?50⍴26] ⍝ use only 50 chars because of mantis 18309 - 1.8E¯71 chance of existing name
:If 1≠≢win ⋄ 'Reformat'Error'Failed to edit a new name' ⋄ :EndIf
win←⊃win ⋄ win.saved←⍬ ⋄ EmptyQueue'Reformat'
Send'["FormatCode",{"win":',(1 ⎕JSON win.id),',"text":',(1 ⎕JSON src),'}]'
(prompt output wins errors)←⍬ 1('Reformat'WaitSub)⍬
:If ~prompt∊¯1 0 1 ⋄ 'Reformat'Error'Produced unexpected prompt: ',⍕prompt
:ElseIf NO_ERROR≢errors ⋄ 'Reformat'Error'Produced unexpected error: ',⍕errors
:ElseIf ''≢output ⋄ 'Reformat'Error'Produced unexpected output: ',⍕output
:ElseIf 1≠≢wins ⋄ 'Reformat'Error'Failed to open 1 window'
:ElseIf wins≡,⊂win ⋄ :AndIf win.saved≡0 ⋄ src←win.text ⋄ ok←1 ⍝ success
⍝ :ElseIf wins.type≡,⊂'Options' ⋄ ∘∘∘ ⋄ CloseWindow⊃wins ⋄ ok←0 ⍝ OptionsDialog to inform of the failure to reformat - can't happen because with attempted to fix it locally beforehand
:Else ⋄ 'Reformat'Error'Unexpected windows'
:EndIf
CloseWindow win
:If ok ⋄ :Leave ⋄ :EndIf
:EndFor
:If ~ok ⋄ 'Reformat'Error'Failed to reformat: ',⍕src ⋄ :EndIf
∇
∇ fn _TraceChangedLine win;errors;ok;output;prompt;wins
⍝ Wait for tracer to change its line - will wait forever if it doesn't happen
(prompt output wins errors)←⍬ win(fn WaitSub)'SetHighlightLine'
ok←(prompt∊¯1 0 1)∧(output≡'')∧(errors≡NO_ERROR)∧(wins≡,⊂win)
ok∧←(IsWin win)∧('Tracer'≡win.type) ⍝ edit turned back into a tracer
:If ~ok ⋄ fn Error'Failed to changed Tracer line' ⋄ :EndIf
∇
∇ TraceCutback win;errors;output;wins
⍝ Cut back to caller (no code execution)
:Access Public
Send'["Cutback",{"win":',(1 ⎕JSON win.id),'}]'
'TraceCutback'_TraceChangedLine win
∇
∇ TraceNext win
⍝ Jump to next tracer line (no code execution)
:Access Public
Send'["TraceForward",{"win":',(1 ⎕JSON win.id),'}]'
'TraceNext'_TraceChangedLine win
∇
∇ TracePrev win
⍝ Jump to previous tracer line (no code execution)
:Access Public
Send'["TraceBackward",{"win":',(1 ⎕JSON win.id),'}]'
'TracePrev'_TraceChangedLine win
∇
∇ win SetStops stops
⍝ Change stop points of trace or edit window
:Access Public
:If ~IsStops stops←,stops ⋄ 'SetStops'Error'Right argument must be numeric vector: ',⍕stops ⋄ :EndIf
:If ~IsWin win ⋄ 'SetStops'Error'Left argument must be a window' ⋄ :EndIf
EmptyQueue'SetStops'
Send'["SetLineAttributes",{"win":',(1 ⎕JSON win.id),',"stop":',(1 ⎕JSON stops),'}]'
TIMEOUT EmptyQueue'SetStops' ⍝ no response to this message
win.stop←stops
∇
∇ (traces stops monitors)←ClearTraceStopMonitor;messages
⍝ Clear all traces/stops/monitors in active workspace
:Access Public
EmptyQueue'ClearTraceStopMonitor'
Send'["ClearTraceStopMonitor",{"token":0}]'
messages←('ClearTraceStopMonitor'WaitFor 1)'ReplyClearTraceStopMonitor'
(traces stops monitors)←(2⊃⊃messages).(traces stops monitors)
∇
∇ ok←_RunQA stop;BUG;_Reformat;debug;dup;dup2;dupstops;dupwin;error;foo;foowin;goo;goowin;html;ok;ondisk;ondisk2;output;r;src;src1;src2;stops;tmp;tmpdir;tmpfile;win;win2;∆
:Access Public
∆←stop{⍺←'' ⋄ ⍵≡1:⍵ ⋄ ⍺⍺:0⊣'QA'Error ⍺ ⋄ 0⊣'QA'LogWarn ⍺}
ok←1
debug←DEBUG ⋄ DEBUG←1 ⍝ maximise the likelihood of missing messages
ok∧←'Execute )CLEAR'∆ 1('clear ws',NL)(NO_WIN)(NO_ERROR)≡Execute')CLEAR'
ok∧←'Execute ]rows on'∆(r[2]∊'Was ON' 'Was OFF',¨NL)∧(1(NO_WIN)(NO_ERROR)≡(⊂1 3 4)⌷r←Execute']rows on')
⍝ Execution : APL, session, error
ok∧←'Execute meaning of life'∆ 1('42',NL)(NO_WIN)(NO_ERROR)≡Execute'⍎⊖⍕⊃⊂|⌊-*+○⌈×÷!⌽⍉⌹~⍴⍋⍒,⍟?⍳0'
output←∊'DOMAIN ERROR: Divide by zero' ' ÷0' ' ∧',¨NL
error←11 1 'DOMAIN ERROR' 'Divide by zero'
ok∧←'Execute ÷0'∆ 1(output)(NO_WIN)(,⊂error)≡Execute'÷0'
ok∧←error≡GetError
⍝ Reformat
src←' res ← format arg ' ':if arg' '⎕← ''yes''' ':endif'
ok∧←'Reformat function'∆({(⎕NS ⍬).(⎕NR ⎕FX ⍵)}src)≡(Reformat src)
src1←' :Namespace ' '∇ tradfn ' '⎕ ← 1 2 3 ' '∇' 'VAR ← 4 5 6 ' 'dfn ← { ⍺ + ⍵ } ' ' :EndNamespace '
src2←':Namespace' ' ∇ tradfn' ' ⎕←1 2 3' ' ∇' ' VAR ← 4 5 6' ' dfn ← { ⍺ + ⍵ }' ':EndNamespace'
ok∧←'Reformat namespace'∆ src2≡Reformat src1
dup←' res←dup arg' ' ⎕←''this is dup''' ' res←arg arg'
foo←' res←foo arg' ' ⎕←''this is foo''' ' res←dup arg'
goo←' res←goo arg' ' ⎕←''this is goo''' ' res←foo arg'
win←EditOpen'dup'
ok∧←'EditOpen on same window'∆ win≡EditOpen'dup'
ok∧←'ED on same window'∆ win≡ED'dup'
ok∧←'EditFix from scratch'∆ win EditFix dup
ok∧←'EditFix from scratch effect'∆ 1(,(↑dup),NL)(NO_WIN)(NO_ERROR)≡Execute' ⎕CR''dup'' '
CloseWindow win
win←EditOpen'dup'
ok∧←'EditOpen source'∆ dup≡win.text
ok∧←'EditFix changing name'∆ win EditFix foo
ok∧←'EditFix changing name effect'∆ 1(,(↑foo),NL)(NO_WIN)(NO_ERROR)≡Execute' ⎕CR''foo'' '
ok∧←'EditOpen on previous name'∆ win≡dupwin←EditOpen'dup' ⍝ mantis 18291 - opens the window where foo was fixed because the interpreter thinks it's still dup
ok∧←'EditOpen on fixed name'∆ win≢foowin←EditOpen'foo' ⍝ mantis 18291 - opens a new window rather than go to window where foo was fixed
CloseAllWindows ⍝ close two windows - would error on failure
CloseAllWindows ⍝ close zero window - would error on failure
ok∧←'Execute ⎕FX goo'∆ 1('goo',NL)(NO_WIN)(NO_ERROR)≡Execute'+⎕FX ',⍕Stringify¨goo ⍝ goo → foo → dup
⍝ Tracer
ok∧←'Trace foo'∆ 1('')(WINS)(NO_ERROR)≡Trace'foo ''argument'' '
win←⊃WINS
ok∧←'TraceResume foo'∆ 1('this is foo',NL,'this is dup',NL,' argument argument ',NL)(,⊂win)(NO_ERROR)≡TraceResume win ⍝ will close window
ok∧←'TraceResume effect'∆ WINS≡NO_WIN
ok∧←'Execute ⎕STOP goo'∆ 1('1',NL)(NO_WIN)(NO_ERROR)≡Execute'+1 ⎕STOP ''goo''' ⍝ set breakpoint
ok∧←'Execute goo'∆ 1(NL,'goo[1]',NL)(WINS)(ERROR_BREAK)≡Execute'goo ''hello''' ⍝ pop up tracer on breakpoint
win←⊃WINS
ok∧←'Tracing goo[1]'∆('goo' 1 'Tracer'goo(,1))(,win)≡win.(title line type text stop)WINS
ok∧←'Editing while tracing goo[1]'∆ win≡EditOpen'goo'
ok∧←'Editing goo[1]'∆('goo' 1 'Editor'goo(,1))(,win)≡win.(title line type text stop)WINS
CloseWindow win
ok∧←'Closing goo[1] editor back into tracer'∆('goo' 1 'Tracer'goo(,1))(,win)≡win.(title line type text stop)WINS
ok∧←'TraceInto goo[1]'∆ 1('this is goo',NL)(,win)(NO_ERROR)≡TraceInto win ⍝ goo[1]
ok∧←'Tracing goo[2]'∆('goo' 2 'Tracer'goo(,1))(,win)≡win.(title line type text stop)WINS
ok∧←'TraceInto goo[2]'∆ 1('')(,win)(NO_ERROR)≡TraceInto win ⍝ goo[2] → foo[1]
ok∧←'Tracing foo[1]'∆('foo' 1 'Tracer'foo ⍬)(,win)≡win.(title line type text stop)WINS
TraceCutback win ⍝ → goo[2]
ok∧←'Tracing goo[2]'∆('goo' 2 'Tracer'goo(,1))(,win)≡win.(title line type text stop)WINS
win2←EditOpen'dup'
ok∧←'Tracing goo[2] and editing dup'∆('goo' 2 'Tracer'goo(,1))('dup' 0 'Editor'dup ⍬)(win win2)≡win.(title line type text stop)win2.(title line type text stop)WINS
ok∧←'Fixing dup + stops'∆ win2 EditFix(dup2←' dup' ' dup1' ' dup2')(0 1 2)
ok∧←'Tracing goo[2] and fixed dup + stops'∆('goo' 2 'Tracer'goo(,1))('dup' 0 'Editor'dup2(0 1 2))(win win2)≡win.(title line type text stop)win2.(title line type text stop)WINS
ok∧←'Fixing dup source effect'∆ 1(,(↑dup2),NL)(NO_WIN)(NO_ERROR)≡Execute' ⎕CR''dup'' '
ok∧←'Fixing dup stops effect'∆ 1('0 1 2',NL)(NO_WIN)(NO_ERROR)≡Execute' ⎕STOP''dup'' '
ok∧←'Unfixing dup + stops'∆ win2 EditFix dup ⍬
ok∧←'Tracing goo[2] and unfixed dup + stops'∆('goo' 2 'Tracer'goo(,1))('dup' 0 'Editor'dup ⍬)(win win2)≡win.(title line type text stop)win2.(title line type text stop)WINS
ok∧←'Unfixing dup source effect'∆ 1(,(↑dup),NL)(NO_WIN)(NO_ERROR)≡Execute' ⎕CR''dup'' '