1 /// `moduleView` is a work-in-progress component used to display an overview of a module.
2 ///
3 /// Warning: This module is unstable. Significant changes may be made without prior warning.
4 ///
5 /// This module is not enabled, unless additonal dependencies, `fluid-tree-sitter` and `fluid-tree-sitter:d` are also
6 /// compiled in.
7 module fluid.module_view;
8 
9 version (Fluid_ModuleView):
10 version (Have_fluid_tree_sitter):
11 version (Have_fluid_tree_sitter_d):
12 
13 debug (Fluid_BuildMessages) {
14     pragma(msg, "Fluid: Including moduleView");
15 }
16 
17 import lib_tree_sitter;
18 import bindbc = bindbc.loader;
19 
20 import std.conv;
21 import std.range;
22 import std.format;
23 import std.string;
24 import std.algorithm;
25 
26 import fluid.node;
27 import fluid.slot;
28 import fluid.rope;
29 import fluid.label;
30 import fluid.space;
31 import fluid.style;
32 import fluid.frame;
33 import fluid.button;
34 import fluid.structs;
35 import fluid.popup_button;
36 import fluid.code_input;
37 import fluid.tree_sitter;
38 
39 
40 @safe:
41 
42 
43 private {
44 
45     bool initialized;
46     TSQuery* documentationQuery;
47     TSQuery* dlangQuery;
48 
49 }
50 
51 static this() @system {
52 
53     // Guard to keep this only running once
54     if (initialized) return;
55     initialized = true;
56 
57     TSQueryError error;
58     uint errorOffset;
59 
60     auto language = treeSitterLanguage!"d";
61     auto query = q{
62         (
63             (comment)+ @comment
64         )
65     };
66 
67     documentationQuery = ts_query_new(language, query.ptr, cast(uint) query.length, &errorOffset, &error);
68     assert(documentationQuery, format!"%s at offset %s"(error, errorOffset));
69 
70     dlangQuery = ts_query_new(language, dQuerySource.ptr, cast(uint) dQuerySource.length, &errorOffset, &error);
71     assert(dlangQuery, format!"%s at offset %s"(error, errorOffset));
72 
73 }
74 
75 static ~this() @system {
76 
77     // Guard to keep this only running once
78     if (!initialized) return;
79     initialized = false;
80 
81     ts_query_delete(documentationQuery);
82     ts_query_delete(dlangQuery);
83 
84 }
85 
86 /// Provides information about the companion D compiler to use for evaluating examples.
87 struct DlangCompiler {
88 
89     import std.regex;
90     import std.process;
91 
92     enum Type {
93         dmd,
94         ldc,
95     }
96 
97     /// Type (vendor) of the compiler. Either DMD or LDC.
98     Type type;
99 
100     /// Executable name or path to the compiler executable. If null, no compiler is available.
101     string executable;
102 
103     /// DMD frontend version of the compiler. Major version is assumed to be 2.
104     int frontendMinor;
105 
106     /// ditto
107     int frontendPatch;
108 
109     /// ditto
110     enum frontendMajor = 2;
111 
112     /// Import paths to pass to the compiler.
113     string[] importPaths;
114 
115     /// Returns true if this entry points to a valid compiler.
116     bool opCast(T : bool)() const {
117 
118         return executable !is null
119             && frontendMinor != 0;
120 
121     }
122 
123     /// Find any suitable in the system.
124     static DlangCompiler findAny() {
125 
126         return either(
127             findDMD,
128             findLDC,
129         );
130 
131     }
132 
133     /// Find DMD in the system.
134     static DlangCompiler findDMD() {
135 
136         // According to run.dlang.io, this pattern has been used since at least 2.068.2
137         auto pattern = regex(r"D Compiler v2.(\d+).(\d+)");
138         auto explicitDMD = std.process.environment.get("DMD");
139         auto candidates = explicitDMD.empty
140             ? ["dmd"]
141             : [explicitDMD];
142 
143         // Test the executables
144         foreach (name; candidates) {
145 
146             auto process = execute([name, "--version"]);
147 
148             // Found a compatible compiler
149             if (auto match = process.output.matchFirst(pattern)) {
150 
151                 return DlangCompiler(Type.dmd, name, match[1].to!int, match[2].to!int);
152 
153             }
154 
155         }
156 
157         return DlangCompiler.init;
158 
159     }
160 
161     /// Find LDC in the system.
162     static DlangCompiler findLDC() {
163 
164         // This pattern appears to be stable as, according to the blame, hasn't changed in at least 5 years
165         auto pattern = regex(r"based on DMD v2\.(\d+)\.(\d+)");
166         auto explicitLDC = std.process.environment.get("LDC");
167         auto candidates = explicitLDC.empty
168             ? ["ldc2", "ldc"]
169             : [explicitLDC];
170 
171         // Test the executables
172         foreach (name; candidates) {
173 
174             auto process = execute([name, "--version"]);
175 
176             // Found a compatible compiler
177             if (auto match = process.output.matchFirst(pattern)) {
178 
179                 return DlangCompiler(Type.ldc, name, match[1].to!int, match[2].to!int);
180 
181             }
182 
183         }
184 
185         return DlangCompiler.init;
186 
187     }
188 
189     unittest {
190 
191         import std.stdio;
192 
193         auto dmd = findDMD();
194         auto ldc = findLDC();
195 
196         // Output search results
197         // Correctness of these tests has to be verified by the CI runner script or the programmer
198         if (dmd) {
199             writefln!"Found DMD (%s) 2.%s.%s"(dmd.executable, dmd.frontendMinor, dmd.frontendPatch);
200             assert(dmd.type == Type.dmd);
201         }
202         else
203             writefln!"DMD wasn't found";
204 
205         if (ldc) {
206             writefln!"Found LDC (%s) compatible with DMD 2.%s.%s"(ldc.executable, ldc.frontendMinor, ldc.frontendPatch);
207             assert(ldc.type == Type.ldc);
208         }
209         else
210             writefln!"LDC wasn't found";
211 
212         // Leading zeros have to be ignored
213         assert("068".to!int == 68);
214 
215         // Compare results of the compiler-agnostic and compiler-specific functions
216         if (auto compiler = findAny()) {
217 
218             final switch (compiler.type) {
219 
220                 case Type.dmd:
221                     assert(dmd);
222                     break;
223 
224                 case Type.ldc:
225                     assert(ldc);
226                     break;
227 
228             }
229 
230         }
231 
232         // No compiler found
233         else {
234 
235             assert(!dmd);
236             assert(!ldc);
237 
238         }
239 
240     }
241 
242     /// Get the flag for importing from given directory.
243     string importFlag(string directory) const {
244 
245         return "-I" ~ directory;
246 
247     }
248 
249     /// Get the flag for adding all import directories specified in compiler config.
250     string[] importPathsFlag() const {
251 
252         return importPaths
253             .map!(a => importFlag(a))
254             .array;
255 
256     }
257 
258     /// Get the flag to generate a shared library for the given compiler.
259     string sharedLibraryFlag() const {
260 
261         final switch (type) {
262 
263             case Type.dmd: return "-shared";
264             case Type.ldc: return "--shared";
265 
266         }
267 
268     }
269 
270     /// Get the flag to include unittests.
271     string unittestFlag() const {
272 
273         final switch (type) {
274 
275             case Type.dmd: return "-unittest";
276             case Type.ldc: return "--unittest";
277 
278         }
279 
280     }
281 
282     /// Compile a shared library from given source file.
283     ///
284     /// TODO make this async
285     auto compileSharedLibrary(string source, out string outputPath) const
286     in (this)
287     do {
288 
289         import fs = std.file;
290         import random = std.random;
291         import std.path : buildPath, setExtension;
292 
293         static string path;
294 
295         // Build a path to contain the program's source
296         if (!path)
297             path = fs.tempDir.buildPath("fluid_" ~ random.uniform!uint.to!string ~ ".d");
298 
299         // Write the source
300         fs.write(path, source);
301 
302         // Find the correct extension for the system
303         version (Windows)
304             outputPath = path.setExtension(".dll");
305         else version (OSX)
306             outputPath = path.setExtension(".dylib");
307         else
308             outputPath = path.setExtension(".so");
309 
310         auto cmdline = [executable, sharedLibraryFlag, unittestFlag, path, "-of=" ~ outputPath] ~ importPathsFlag;
311 
312         debug (Fluid_BuildMessages) {
313             import std.stdio;
314             writefln!"Fluid: compiling $ %s"(escapeShellCommand(cmdline));
315         }
316 
317         // Compile the program
318         return execute(cmdline);
319 
320     }
321 
322 }
323 
324 /// Create an overview display of the given module.
325 Frame moduleViewSource(Params...)(Params params, DlangCompiler compiler, string source,
326     Theme contentTheme = fluidDefaultTheme) @trusted
327 do {
328 
329     auto language = treeSitterLanguage!"d";
330     auto parser = ts_parser_new();
331     scope (exit) ts_parser_delete(parser);
332 
333     ts_parser_set_language(parser, language);
334 
335     // Parse the source
336     auto tree = ts_parser_parse_string(parser, null, source.ptr, cast(uint) source.length);
337     scope (exit) ts_tree_delete(tree);
338     auto root = ts_tree_root_node(tree);
339     auto cursor = ts_query_cursor_new();
340     scope (exit) ts_query_cursor_delete(cursor);
341 
342     auto view = ModuleView(compiler, source, contentTheme);
343     auto result = vframe(params);
344 
345     // Perform a query to find possibly relevant comments
346     ts_query_cursor_exec(cursor, documentationQuery, root);
347     TSQueryMatch match;
348     captures: while (ts_query_cursor_next_match(cursor, &match)) {
349 
350         auto captures = match.captures[0 .. match.capture_count];
351         auto node = captures[$-1].node;
352 
353         // Load the comment
354         auto docs = readDocs(source, captures)
355             .interpretDocs;
356 
357         // Find the symbol the comment is attached to
358         while (true) {
359 
360             node = ts_node_next_named_sibling(node);
361             if (ts_node_is_null(node)) break;
362 
363             // Once found, annotate and append to result
364             if (auto annotated = view.annotate(docs, node)) {
365                 result ~= annotated;
366                 continue captures;
367             }
368 
369         }
370 
371         // Nothing relevant found, paste the documentation as-is
372         result ~= docs;
373 
374     }
375 
376     return result;
377 
378 }
379 
380 /// Load the module source from a file.
381 Frame moduleViewFile(Params...)(Params params, DlangCompiler compiler, string filename,
382     Theme contentTheme = fluidDefaultTheme)
383 do {
384 
385     import std.file : readText;
386 
387     return moduleViewSource!Params(params, compiler, filename.readText, contentTheme);
388 
389 }
390 
391 private struct ModuleView {
392 
393     DlangCompiler compiler;
394     string source;
395     Theme contentTheme;
396     int unittestNumber;
397 
398     /// Returns:
399     ///     Space to represent the node in the output, or `null` if the given TSNode doesn't correspond to any known valid
400     ///     symbol.
401     Space annotate(Space documentation, TSNode node) @trusted {
402 
403         const typeC = ts_node_type(node);
404         const type = typeC[0 .. strlen(typeC)];
405 
406         const start = ts_node_start_byte(node);
407         const end = ts_node_end_byte(node);
408         const symbolSource = source[start .. end];
409 
410         switch (type) {
411 
412             // unittest
413             case "unittest_declaration":
414 
415                 // Create the code block
416                 auto input = dlangInput();
417 
418                 // Find the surrounding context
419                 const exampleStart = start + symbolSource.countUntil("{") + 1;
420                 const exampleEnd = end - symbolSource.retro.countUntil("}") - 1;
421 
422                 // Set a constant mangle for the example by injecting it into source
423                 const injectSource = Rope(q{
424                     pragma(mangle, "fluid_moduleView_entrypoint")
425                 });
426 
427                 const prefix = source[0 .. start] ~ injectSource ~ source[start .. exampleStart];
428                 const value = source[exampleStart .. exampleEnd];
429                 const suffix = Rope(source[exampleEnd .. $]);
430 
431                 // Append code editor to the result
432                 documentation.children ~= exampleView(compiler, prefix, value, suffix, contentTheme);
433                 return documentation;
434 
435             // Declarations that aren't implemented
436             case "module_declaration":
437             case "import_declaration":
438             case "mixin_declaration":
439             case "variable_declaration":
440             case "auto_declaration":
441             case "alias_declaration":
442             case "attribute_declaration":
443             case "pragma_declaration":
444             case "struct_declaration":
445             case "union_declaration":
446             case "invariant_declaration":
447             case "class_declaration":
448             case "interface_declaration":
449             case "enum_declaration":
450             case "anonymous_enum_declaration":
451             case "function_declaration":
452             case "template_declaration":
453             case "mixin_template_declaration":
454                 return documentation;
455 
456             // Unknown declaration, skip
457             default:
458                 return null;
459 
460         }
461 
462     }
463 
464 }
465 
466 /// Produce an example.
467 Frame exampleView(DlangCompiler compiler, CodeInput input, Theme contentTheme) {
468 
469     auto stdoutLabel = label(.layout!"fill", "");
470     auto resultCanvas = nodeSlot!Frame(.layout!"fill");
471 
472     /// Wrapper over bindbc.SharedLib to ensure proper library destruction.
473     struct SharedLib {
474 
475         bindbc.SharedLib library;
476         alias library this;
477 
478         ~this() {
479             clear();
480         }
481 
482         void clear() @trusted {
483             if (library != bindbc.invalidHandle) {
484                 bindbc.unload(library);
485             }
486         }
487 
488     }
489 
490     auto library = new SharedLib;
491     auto originalValue = input.value;
492 
493     /// Compile the program
494     void compileAndRun() {
495 
496         string outputPath;
497 
498         // Unload the previous library
499         library.clear();
500 
501         auto result = compiler.compileSharedLibrary(input.sourceValue.to!string, outputPath);
502 
503         // Compiled successfully
504         if (result.status == 0) {
505 
506             // Prepare the output
507             resultCanvas.value = vframe(
508                 .layout!(1, "fill"),
509                 contentTheme,
510             );
511             resultCanvas.show();
512             stdoutLabel.hide();
513 
514             // Run the snippet and output the results
515             mockRun((node) {
516                 resultCanvas.value ~= node;
517             });
518             scope (exit) mockRun(null);
519 
520             // TODO Run the result on a separate thread to prevent the app from locking up
521             library.library = runSharedLibrary(outputPath);
522             resultCanvas.updateSize();
523 
524         }
525 
526         // Write compiler output
527         else {
528 
529             stdoutLabel.show();
530             stdoutLabel.text = result.output;
531             resultCanvas.hide();
532 
533         }
534 
535     }
536 
537     /// Restore original source code.
538     void restoreSource() {
539 
540         input.value = originalValue;
541         input.updateSize();
542 
543     }
544 
545     // Compile and run the program; do the same when submitted
546     if (compiler) {
547 
548         compileAndRun();
549         input.submitted = &compileAndRun;
550 
551         Frame root;
552 
553         return root = hframe(
554             .layout!"fill",
555             vspace(
556                 .layout!(1, "fill"),
557                 hframe(
558                     button("Run", () => compileAndRun()),
559                     popupButton("Reset",
560                         label("Reset original content? Changes\n"
561                             ~ "will not be saved."),
562                         hspace(
563                             .layout!"end",
564                             button("Cancel", delegate {
565 
566                                 root.tree.focus = null;
567 
568                             }),
569                             button("Reset content", delegate {
570 
571                                 restoreSource();
572                                 root.tree.focus = null;
573 
574                             }),
575                         ),
576                     ),
577                 ),
578                 input,
579             ),
580             vspace(
581                 .layout!(1, "fill"),
582                 resultCanvas,
583                 stdoutLabel,
584             ),
585         );
586 
587     }
588 
589     // Disable edits if there's no compiler available
590     else return hframe(
591         .layout!"fill",
592         input.disable(),
593     );
594 
595 }
596 
597 /// ditto
598 Frame exampleView(DlangCompiler compiler, Rope prefix, string value, Rope suffix, Theme contentTheme) {
599 
600     auto input = dlangInput();
601     input.prefix = prefix ~ "\n";
602     input.suffix = "\n" ~ suffix;
603     input.value = value
604         .outdent
605         .strip;
606 
607     return exampleView(compiler, input, contentTheme);
608 
609 }
610 
611 /// Run a Fluid snippet from a shared library.
612 private bindbc.SharedLib runSharedLibrary(string path) @trusted {
613 
614     // Load the resulting library
615     auto library = bindbc.load(path.toStringz);
616 
617     // Failed to load
618     if (library == bindbc.invalidHandle) {
619 
620         foreach (error; bindbc.errors)
621             printf("%s %s", error.error, error.message);
622 
623         return library;
624 
625     }
626 
627     void function() entrypoint;
628     bindbc.bindSymbol(library, cast(void**) &entrypoint, "fluid_moduleView_entrypoint");
629     entrypoint();
630 
631     return library;
632 
633 }
634 
635 /// Creates a `CodeInput` with D syntax highlighting.
636 CodeInput dlangInput(void delegate() @safe submitted = null) @trusted {
637 
638     auto language = treeSitterLanguage!"d";
639     auto highlighter = new TreeSitterHighlighter(language, dlangQuery);
640 
641     return codeInput(
642         .layout!"fill",
643         highlighter,
644         submitted
645     );
646 
647 }
648 
649 private Rope readDocs(string source, TSQueryCapture[] captures) @trusted {
650 
651     import std.stdio : writefln;
652 
653     const lineFeed = Rope("\n");
654 
655     Rope result;
656 
657     // Load all comments
658     foreach (capture; captures) {
659 
660         auto start = ts_node_start_byte(capture.node);
661         auto end = ts_node_end_byte(capture.node);
662         auto commentSource = source[start .. end];
663 
664         // TODO multiline comments
665         // Filter
666         if (!commentSource.skipOver("///")) continue;
667 
668         result ~= Rope(Rope(commentSource), lineFeed);
669 
670     }
671 
672     return result;
673 
674 }
675 
676 private Space interpretDocs(Rope rope) {
677 
678     import std.conv : to;
679     import fluid.typeface : Typeface;
680 
681     const space = Rope(" ");
682     const lineFeed = Rope("\n");
683 
684     rope = rope.strip;
685 
686     // Empty comment, omit
687     if (rope == "") return vspace();
688 
689     // Ditto comment, TODO
690     if (rope == "ditto") return vspace();
691 
692     // TODO DDoc
693     CodeInput lastCode;
694     auto lastParagraph = label("");
695     auto result = vspace(
696         .layout!"fill",
697         lastParagraph
698     );
699 
700     string preformattedDelimiter;
701 
702     // Read line by line
703     foreach (line; Typeface.lineSplitter(rope)) {
704 
705         // Regular documentation line
706         if (preformattedDelimiter.empty) {
707 
708             line = line.strip();
709 
710             // Start a new paragraph if the line is blank
711             if (line.empty) {
712                 if (!lastParagraph.text.empty)
713                     result ~= lastParagraph = label("");
714             }
715 
716             // Preformatted line
717             // TODO other delimiters
718             // TODO common space (prefix)
719             else if (line == "---") {
720                 preformattedDelimiter = "---";
721                 result ~= lastCode = dlangInput().disable();
722             }
723 
724             // Append text to previous line
725             else {
726                 lastParagraph.text ~= Rope(line, space);
727             }
728 
729         }
730 
731         // Preformatted fragments/code
732         else {
733 
734             // Reached the other delimiter, turn preformatted lines off
735             if (line.strip == preformattedDelimiter) {
736                 preformattedDelimiter = null;
737                 result ~= lastParagraph = label("");
738                 lastCode.value = lastCode.value.to!string.outdent;
739             }
740 
741             /// Append text to previous line
742             else {
743                 lastCode.push(Rope(line, lineFeed));
744             }
745 
746         }
747 
748     }
749 
750     return result;
751 
752 }
753 
754 /// Outdent a rope.
755 ///
756 /// A saner wrapper over `std.string.outdent` that actually does what it should do.
757 private string outdent(string rope) {
758 
759     import std.string : outdent;
760 
761     return rope.splitLines.outdent.join("\n");
762 
763 }