diff --git a/base.rkt b/base.rkt index 28b5ec7..7247788 100644 --- a/base.rkt +++ b/base.rkt @@ -1,205 +1,12 @@ -#lang at-exp racket/base -(require racket/contract - racket/dict - racket/format - racket/file - racket/path - racket/runtime-path - compiler/compilation-path - compiler/cm - "exn-gobbler.rkt") - -(provide (all-defined-out)) - -(module+ test - (require rackunit)) - -(define version-bytes (string->bytes/utf-8 (version))) -(define vm-bytes (string->bytes/utf-8 (symbol->string (system-type 'vm)))) - -(define-logger quickscript) - -;; TODO: What if (find-system-path 'pref-dir) does not exist? -(define quickscript-dir - (or (getenv "PLTQUICKSCRIPTDIR") - (build-path (find-system-path 'pref-dir) "quickscript"))) - -(define library-file - (build-path quickscript-dir "library.rktd")) - -(define user-script-dir - (build-path quickscript-dir "user-scripts")) - -(define (path-free? p-str) - (not (path-only p-str))) - -(define (path-string->string p-str) - (if (string? p-str) - p-str - (path->string p-str))) - -(define (script-file? f) - (equal? (path-get-extension f) #".rkt")) - -(define (path-string=? dir1 dir2) - (string=? (path-string->string dir1) - (path-string->string dir2))) - -(module+ test - - (check-true (path-free? "a.rkt")) - (check-false (path-free? "b/a.rkt")) - (check-false (path-free? "b/a")) - (when (eq? (system-path-convention-type) 'unix) - (check-true - (path-string=? "a/b/c.rkt" - (build-path "a" "b/c.rkt"))))) - -(define-syntax-rule (time-info str body ...) - (let ([ms (current-milliseconds)]) - (log-quickscript-info (string-append "Begin: " str "...")) - (begin0 - (begin body ...) - (log-quickscript-info - (string-append "End : " str ". Took " (number->string (- (current-milliseconds) ms)) "ms"))))) - -(define props-default - `((name . #f) - (filepath . #f) - (label . #f) ; Should be mandatory - (menu-path . ()) - (shortcut . #f) - (shortcut-prefix . #f) ; should be (get-default-shortcut-prefix), but this depends on gui/base - (help-string . "My amazing script") - (output-to . selection) ; outputs the result in a new tab - (persistent? . #f) - (os-types . (unix macosx windows)) ; list of supported os types - )) - -(define this-os-type (system-type 'os)) - -;; proc-name : string? -;; label : string? -;; TODO: extend this with a given property-dict -(define (make-simple-script-string proc-name label - #:script-help-string [script-help-string #f]) - ;; See the manual in the Scripts|Manage Scripts|Help menu for more information. - @string-append{ #lang racket/base - -(require quickscript) -@(if script-help-string (string-append "\n(script-help-string " (~s script-help-string) ")\n") "") -;; Returns a replacement string for the selected string `selection` -;; ("" if no text is selected), or `#f` to leave the selection as is. -(define-script @proc-name - #:label "@label" - (λ (selection) - #f)) -}) - -;; script-filename : path-string? -(define (make-submod-path script-filename) - (list 'submod - (list 'file (path-string->string script-filename)) - 'script-info)) - -;; script-filename : path-string? -;; Returns #f or a string. -;; Important: see note for get-property-dicts -(define (get-script-help-string script-filename) - (dynamic-require (make-submod-path script-filename) - 'quickscript-module-help-string - (λ () #f))) - -(define (property-dict? v) - (and (dict? v) - (dict-has-key? v 'label))) - -;; Returns a list of dictionaries of the properties of the scripts in script-filename, -;; augmented with the scripts' function and the script filepath. -;; IMPORTANT: Loads the file in the current namespace, so a new namespace should probably -;; be created with (make-base-empty-namespace). -;; script-filename : path-string? -(define (get-property-dicts script-filepath) - ; Ensure the script is compiled for the correct version of Racket - (compile-user-script script-filepath) - - (define the-submod (make-submod-path script-filepath)) - (dynamic-require the-submod #f) - (define-values (vars syntaxes) (module->exports the-submod)) - (define funs (map car (dict-ref vars 0))) - (define property-dicts - (filter values - (for/list ([fun (in-list funs)]) - (define maybe-props (dynamic-require the-submod fun)) - (and (property-dict? maybe-props) - (list* - (cons 'name fun) - (cons 'filepath script-filepath) - maybe-props))))) - property-dicts) - -(define (prop-dict-ref props key) - (dict-ref props key (dict-ref props-default key))) - -(module+ test - (require racket/file) - (define dir (find-system-path 'temp-dir)) - (define filename "tmp-script.rkt") - (define filepath (build-path dir filename)) - (define proc-sym 'my-first-script) - (define proc-name (symbol->string proc-sym)) - (define label "My First Script") - (define help-str "The help-string of the script.") - (display-to-file (make-simple-script-string proc-name label - #:script-help-string help-str) - filepath - #:exists 'replace) - - ; Note: because the script requires `quickscript/script`, - ; quickscript must be installed as package/collection for the following to work. - (define-values (prop-dicts help-str2) - (parameterize ([current-namespace (make-base-empty-namespace)]) - (values (get-property-dicts filepath) - (get-script-help-string filepath)))) - (check = (length prop-dicts) 1) - (define props (car prop-dicts)) - (check string=? - (dict-ref props 'label) - label) - (check eq? - (prop-dict-ref props 'name) - proc-sym) - (check string=? - help-str2 - help-str)) - -;===================; -;=== Compilation ===; -;===================; - -(define/contract (compile-user-script file) - (-> path-string? any) - - ;; Simple wrapper for now, but may be specialized for efficiency later. - (void) - #;(compile-user-scripts (list file))) - -(define/contract (compile-user-scripts files - #:exn-gobbler [gb (make-exn-gobbler "Compiling scripts")]) - (->* [(listof path-string?)] - [#:exn-gobbler exn-gobbler?] - exn-gobbler?) - - ; Synchronous version: - (parameterize ([current-namespace (make-base-empty-namespace)]) - (define cmc (make-caching-managed-compile-zo)) - (for ([f (in-list files)]) - (with-handlers* ([exn:fail? (λ (e) (gobble gb e (path->string f)))]) - (time-info (format "Compiling ~a" (path->string f)) - (cmc f))))) - (log-quickscript-info (exn-gobbler->string gb)) - gb) - -(define (zo-file src-file) - (get-compilation-bytecode-file src-file #:modes '("compiled"))) +(require "private/base.rkt" + (for-syntax racket/base + syntax/transformer) + (prefix-in lib: "private/library.rkt")) +(provide get-script-help-string + script-file? + user-script-dir) +(define-syntax user-script-dir + (make-variable-like-transformer #'(get-user-script-dir))) +(define (get-user-script-dir) + (lib:user-script-dir (lib:load))) diff --git a/info.rkt b/info.rkt index 7641a6c..a287c33 100644 --- a/info.rkt +++ b/info.rkt @@ -1,9 +1,23 @@ #lang info +(define pkg-name "quickscript") +(define collection "quickscript") +(define name "Quickscript") +(define version "1.1") +(define pkg-desc "Scripting engine for DrRacket.") +(define license + '(Apache-2.0 OR MIT)) + +(define drracket-tools '(("tool.rkt"))) +(define drracket-tool-names '("Quickscript")) +(define drracket-tool-icons '(#f)) + +(define scribblings + '(("scribblings/quickscript.scrbl" () (drracket-plugin) "quickscript"))) + (define deps - '("base" + '(["base" #:version "8.13.0.2"] ; for path equal-always? fix "drracket-plugin-lib" - "drracket" "gui-lib" "net-lib" "scribble-lib")) @@ -13,18 +27,4 @@ "drracket" "gui-doc" "racket-doc" - "draw-doc" "rackunit-lib")) - -(define name "Quickscript") -(define drracket-tools '(("tool.rkt"))) -(define drracket-tool-names '("Quickscript")) -(define drracket-tool-icons '(#f)) - -(define scribblings '(("scribblings/quickscript.scrbl" () (drracket-plugin) "quickscript"))) - -(define compile-omit-paths - '()) - -(define license - '(Apache-2.0 OR MIT)) diff --git a/library.rkt b/library.rkt index 95d2173..5c77233 100644 --- a/library.rkt +++ b/library.rkt @@ -1,130 +1,16 @@ #lang racket/base -(require racket/contract - racket/dict - racket/file - racket/set - "base.rkt" - ) -;; A library is a hash where a key is a directory (as a string) -;; and a value is a list of files (string without path) to *not* include (called exclusions). -;; That is, by default all non-excluded files are included (in particular the new ones). -(define (new-library) - (define lib (make-hash)) - (add-directory! lib (path->string user-script-dir)) - lib) +;; This module provides limited backwards compatibility for packages +;; that followed the old, broken recommendations for registering scripts. +;; The Quickscript library is now actually implemented in "private/library.rkt". -(define (library? lib) - (hash? lib)) +(require "private/base.rkt") -(define (load [file library-file]) - (if (file-exists? file) - (hash-copy (file->value file)) - (new-library))) - -(define (save! lib [file library-file]) - (make-directory* user-script-dir) - (write-to-file lib file #:exists 'replace)) - -(define (directories lib) - (dict-keys lib)) - -(define (exclusions lib dir #:build? [build? #f]) - (define exs (dict-ref lib (path-string->string dir) '())) - (if build? - (map (λ (x) (build-path dir x)) exs) - exs)) - -;; Returns the list of script files in the given directory. -;; If exclude is not #f, then only such files that are not listed as exclusions -;; in the library are returned. -(define (files lib [dir user-script-dir] #:exclude? [exclude? #t]) - (define script-files - (map path->string - (filter (λ (f) (script-file? (build-path dir f))) - (if (directory-exists? dir) - (directory-list dir #:build? #f) - '())))) - (cond [exclude? - (define except-list (exclusions lib dir)) - (set-subtract script-files except-list)] - [else script-files])) - -;; Returns the list full paths of script files --in all listed directories of the library. -;; The keyword argument `exclude?' is as in `files'. -(define (all-files lib #:exclude? [exclude? #t]) - (for*/list ([dir (in-dict-keys lib)] - [f (in-list (files lib dir #:exclude? exclude?))]) - (build-path dir f))) - - -(define (add-directory! lib dir [excl '()]) - (dict-ref! lib (path-string->string dir) excl) - (void)) - -(define (remove-directory! lib dir) - (dict-remove! lib (path-string->string dir))) - -(define (exclude! lib dir filename) - (dict-update! lib - (path-string->string dir) - (λ (excl) (set-add excl filename)))) - -(define (include! lib dir filename) - (dict-update! lib - (path-string->string dir) - (λ (excl) (set-remove excl filename)))) +(provide add-third-party-script-directory! + remove-third-party-script-directory!) (define (add-third-party-script-directory! dir [excl '()]) - (define lib (load)) - (add-directory! lib dir excl) - (save! lib)) + (log-quickscript-error "add-third-party-script-directory! is deprecated and has no effect")) (define (remove-third-party-script-directory! dir) - (define lib (load)) - (remove-directory! lib dir) - (save! lib)) - -(provide/contract - [library? (any/c . -> . boolean?)] - [new-library (-> library?)] - [load ([] - [path-string?] ; does not need to exist - . ->* . library?)] - [save! ([library?] - [path-string?] - . ->* . void?)] - [directories (library? - . -> . (listof string?))] - [exclusions ([library? path-string?] - [#:build? boolean?] - . ->* . (listof path-string?))] - [files ([library?] - [path-string? #:exclude? boolean?] - . ->* . (listof string?))] - [all-files ([library?] - [#:exclude? boolean?] - . ->* . (listof path-string?))] - [add-directory! ([library? - (and/c path-string? absolute-path? directory-exists?)] - [list?] - . ->* . void?)] - [remove-directory! (library? - (and/c path-string? absolute-path?) - . -> . void?)] - [exclude! (library? - (and/c path-string? absolute-path?) - (and/c string? path-free?) - . -> . void?)] - [include! (library? - (and/c path-string? absolute-path?) - (and/c string? path-free?) - . -> . void?)] - [add-third-party-script-directory! - ([(and/c path-string? absolute-path?)] - [(listof (and/c string? path-free?))] - . ->* . void?)] - [remove-third-party-script-directory! - ((and/c path-string? absolute-path?) - . -> . void?)] - ) + (log-quickscript-error "remove-third-party-script-directory! is deprecated and has no effect")) diff --git a/main.rkt b/main.rkt index 14f0a41..4008130 100644 --- a/main.rkt +++ b/main.rkt @@ -1,6 +1,6 @@ #lang racket/base -;;; Re-experts quickscript/script for simplicity in the scripts +;;; Re-exports quickscript/script for simplicity in the scripts (require quickscript/script) (provide (all-from-out quickscript/script)) diff --git a/private/base.rkt b/private/base.rkt new file mode 100644 index 0000000..b14d9a8 --- /dev/null +++ b/private/base.rkt @@ -0,0 +1,215 @@ +#lang at-exp racket/base +(require racket/contract + racket/dict + racket/format + racket/file + racket/path + compiler/compilation-path + compiler/cm + "exn-gobbler.rkt") + +(provide log-quickscript-fatal + log-quickscript-error + log-quickscript-warning + log-quickscript-info + log-quickscript-debug + quickscript-logger + get-script-help-string + make-simple-script-string + prop-dict-ref + #;compile-user-scripts + #;compile-user-script + this-os-type + time-info + path-free? + path-string=? + script-file? + get-property-dicts + path-string->string) + +(module+ test + (require rackunit)) + +(define version-bytes (string->bytes/utf-8 (version))) +(define vm-bytes (string->bytes/utf-8 (symbol->string (system-type 'vm)))) + +(define-logger quickscript) + +(define (path-free? p-str) + (not (path-only p-str))) + +(define (path-string->string p-str) + (if (string? p-str) + p-str + (path->string p-str))) + +(define info.rkt-element (string->path-element "info.rkt")) + +(define (script-file? f) + (and (not (equal? f (if (string? f) + "info.rkt" + info.rkt-element))) + (equal? (path-get-extension f) #".rkt"))) + +(define (path-string=? dir1 dir2) + (string=? (path-string->string dir1) + (path-string->string dir2))) + +(module+ test + + (check-true (path-free? "a.rkt")) + (check-false (path-free? "b/a.rkt")) + (check-false (path-free? "b/a")) + (when (eq? (system-path-convention-type) 'unix) + (check-true + (path-string=? "a/b/c.rkt" + (build-path "a" "b/c.rkt"))))) + +(define-syntax-rule (time-info str body ...) + (let ([ms (current-milliseconds)]) + (log-quickscript-info (string-append "Begin: " str "...")) + (begin0 + (begin body ...) + (log-quickscript-info + (string-append "End : " str ". Took " (number->string (- (current-milliseconds) ms)) "ms"))))) + +(define props-default + `((name . #f) + (filepath . #f) + (label . #f) ; Should be mandatory + (menu-path . ()) + (shortcut . #f) + (shortcut-prefix . #f) ; should be (get-default-shortcut-prefix), but this depends on gui/base + (help-string . "My amazing script") + (output-to . selection) ; outputs the result in a new tab + (persistent? . #f) + (os-types . (unix macosx windows)) ; list of supported os types + )) + +(define this-os-type (system-type 'os)) + +;; proc-name : string? +;; label : string? +;; TODO: extend this with a given property-dict +(define (make-simple-script-string proc-name label + #:script-help-string [script-help-string #f]) + ;; See the manual in the Scripts|Manage Scripts|Help menu for more information. + @string-append{ +#lang racket/base + +(require quickscript) +@(if script-help-string (string-append "\n(script-help-string " (~s script-help-string) ")\n") "") +;; Returns a replacement string for the selected string `selection` +;; ("" if no text is selected), or `#f` to leave the selection as is. +(define-script @proc-name + #:label "@label" + (λ (selection) + #f)) +}) + +;; script-filename : path-string? +(define (make-submod-path script-filename) + (list 'submod + (list 'file (path-string->string script-filename)) ;FIXME + 'script-info)) + +;; script-filename : path-string? +;; Returns #f or a string. +;; Important: see note for get-property-dicts +(define (get-script-help-string script-filename) + (dynamic-require (make-submod-path script-filename) ;FIXME + 'quickscript-module-help-string + (λ () #f))) + +(define (property-dict? v) + (and (dict? v) + (dict-has-key? v 'label))) + +;; Returns a list of dictionaries of the properties of the scripts in script-filename, +;; augmented with the scripts' function and the script filepath. +;; IMPORTANT: Loads the file in the current namespace, so a new namespace should probably +;; be created with (make-base-empty-namespace). +;; script-filename : path-string? +(define (get-property-dicts script-filepath) + ; Ensure the script is compiled for the correct version of Racket + #;(compile-user-script script-filepath) + + (define the-submod (make-submod-path script-filepath)) ;FIXME + (dynamic-require the-submod #f) + (define-values (vars syntaxes) (module->exports the-submod)) + (define funs (map car (dict-ref vars 0))) + (define property-dicts + (filter values + (for/list ([fun (in-list funs)]) + (define maybe-props (dynamic-require the-submod fun)) + (and (property-dict? maybe-props) + (list* + (cons 'name fun) + (cons 'filepath script-filepath) + maybe-props))))) + property-dicts) + +(define (prop-dict-ref props key) + (dict-ref props key (dict-ref props-default key))) + +(module+ test + (require racket/file) + (define dir (find-system-path 'temp-dir)) + (define filename "tmp-script.rkt") + (define filepath (build-path dir filename)) + (define proc-sym 'my-first-script) + (define proc-name (symbol->string proc-sym)) + (define label "My First Script") + (define help-str "The help-string of the script.") + (display-to-file (make-simple-script-string proc-name label + #:script-help-string help-str) + filepath + #:exists 'replace) + + ; Note: because the script requires `quickscript/script`, + ; quickscript must be installed as package/collection for the following to work. + (define-values (prop-dicts help-str2) + (parameterize ([current-namespace (make-base-empty-namespace)]) + (values (get-property-dicts filepath) + (get-script-help-string filepath)))) + (check = (length prop-dicts) 1) + (define props (car prop-dicts)) + (check string=? + (dict-ref props 'label) + label) + (check eq? + (prop-dict-ref props 'name) + proc-sym) + (check string=? + help-str2 + help-str)) + +;===================; +;=== Compilation ===; +;===================; +#; +(define/contract (compile-user-script file) + (-> path-string? any) + + ;; Simple wrapper for now, but may be specialized for efficiency later. + (void) + #;(compile-user-scripts (list file))) +#; +(define/contract (compile-user-scripts files + #:exn-gobbler [gb (make-exn-gobbler "Compiling scripts")]) + (->* [(listof path-string?)] + [#:exn-gobbler exn-gobbler?] + exn-gobbler?) + + ; Synchronous version: + (parameterize ([current-namespace (make-base-empty-namespace)]) + (define cmc (make-caching-managed-compile-zo)) + (for ([f (in-list files)]) + (with-handlers* ([exn:fail? (λ (e) (gobble gb e (path->string f)))]) + (time-info (format "Compiling ~a" (path->string f)) + (cmc f))))) + (log-quickscript-info (exn-gobbler->string gb)) + gb) + +(define (zo-file src-file) + (get-compilation-bytecode-file src-file #:modes '("compiled"))) diff --git a/exn-gobbler.rkt b/private/exn-gobbler.rkt similarity index 100% rename from exn-gobbler.rkt rename to private/exn-gobbler.rkt diff --git a/library-gui.rkt b/private/library-gui.rkt similarity index 69% rename from library-gui.rkt rename to private/library-gui.rkt index 7664702..890e407 100644 --- a/library-gui.rkt +++ b/private/library-gui.rkt @@ -11,52 +11,60 @@ (provide make-library-gui) -(define check-sym #\☑) -(define uncheck-sym #\☐) - -(define (un/checked-file->check+file cf) - (define checked? (char=? check-sym (string-ref cf 0))) - (values checked? (substring cf 2))) - -(define (check+file->un/checked-file c f) - (string-append - (string (if c uncheck-sym check-sym) - #\space) - f)) - -(define (make-library-gui [the-lib-file library-file] - #:parent-frame [parent-frame #f] +(define (check+file->un/checked-file checked? f) + (string-append-immutable (if checked? "☑ " "☐ ") + f)) + +(define data-list-box% + (class list-box% + (init [(d->s datum->string)] + [choices '()]) + (define datum->string d->s) + (super-new [choices '()]) + (set choices) + (inherit append clear get-data get-number get-selection set-selection) + (define/override (set choices) + (clear) + (for ([d (in-list choices)]) + (append (datum->string d) d))) + (define/public (set-datum-selection d) + (unless (for/first ([i (in-range (get-number))] + #:when (equal? d (get-data i))) + (set-selection i) + #t) + (raise-arguments-error '|set-datum-selection in data-list-box%| + "no item matching the given datum" + "given" d))) + (define/public (get-datum-selection) + (define i (get-selection)) + (and i (get-data i))))) + +(define (make-library-gui #:parent-frame [parent-frame #f] #:drracket-parent? [drracket-parent? #f]) ;; Load the files in a new namespace so that if the file is changed ;; the library can pick up the changes. (parameterize ([current-namespace (make-base-empty-namespace)]) (log-quickscript-info "Starting the library GUI.") - (define the-lib (lib:load the-lib-file)) - (define (save!) (lib:save! the-lib the-lib-file)) + (define the-lib (lib:load)) + (define (save! new-lib) + (lib:save! new-lib) + (set! the-lib new-lib)) + (define (user-script-dir) + (lib:user-script-dir the-lib)) (define (files-lb-selection-values) - (define cf (send files-lb get-string-selection)) + (define cf (send files-lb get-datum-selection)) (if cf - (un/checked-file->check+file cf) + (values (car cf) (cdr cf)) (values #f #f))) - (define (set-files-lb dir) - (define files - (if (directory-exists? dir) - (map path->string - (filter (λ (f) (script-file? (build-path dir f))) - (directory-list dir #:build? #f))) - '())) - (define excluded-files (lib:exclusions the-lib dir)) - (send files-lb set - (map (λ (f) (check+file->un/checked-file (member f excluded-files) f)) - files))) + (send files-lb set (lib:directory->enabled+file the-lib dir))) ;; Returns the current selected dir, file and whether it is checked, ;; if all have a value, otherwise returns #f for all 3 values. (define (get-dir+check+file) - (define dir (send dir-lb get-string-selection)) + (define dir (send dir-lb get-datum-selection)) (if dir (let-values ([(checked? file) (files-lb-selection-values)]) (if file @@ -65,25 +73,22 @@ (values #f #f #f))) (define (add-directory [dir #f]) - (unless dir - (set! dir - (get-directory "Choose a script directory to add to the library" - fr - (find-user-pkgs-dir)))) - (when dir - (lib:add-directory! the-lib dir) - (save!) - (reload-dir-lb) - (send dir-lb set-string-selection (path->string dir)) - (dir-lb-select dir))) + (let* ([dir (or dir + (get-directory "Choose a script directory to add to the library" + fr))] + [dir (and dir (path->complete-path (path->directory-path dir)))]) + (when dir + (save! (lib:add-directory the-lib dir)) + (reload-dir-lb) + (send dir-lb set-datum-selection dir) + (dir-lb-select dir)))) (define (remove-directory dir) - (lib:remove-directory! the-lib dir) - (save!) + (save! (lib:remove-directory the-lib dir)) (reload-dir-lb)) (define (remove-selected-dir) - (define dir (send dir-lb get-string-selection)) + (define dir (send dir-lb get-datum-selection)) (when dir (remove-directory dir) (send files-lb clear))) @@ -92,22 +97,22 @@ (define (ex/include-selected-file [force #f]) (define-values (dir checked? file) (get-dir+check+file)) (when file - (cond [(eq? force 'exclude) (lib:exclude! the-lib dir file)] - [(eq? force 'include) (lib:include! the-lib dir file)] - [checked? (lib:exclude! the-lib dir file)] - [else (lib:include! the-lib dir file)]) - (save!) - (define files-lb-selection (send files-lb get-selection)) + (define include? + (case force + [(exclude) #f] + [(include) #t] + [else (not checked?)])) + (save! ((if include? lib:include lib:exclude) the-lib dir file)) (set-files-lb dir) ; Restore the previously selected item - (send files-lb set-selection files-lb-selection)) + (send files-lb set-datum-selection (cons include? file))) (update-bt-files-un/check)) (define (shadow-selected-file) (define-values (dir checked? file) (get-dir+check+file)) (when file (define new-script-path - (build-path user-script-dir file)) + (build-path (user-script-dir) file)) (define proceed? (eq? 'yes (message-box "Create shadow script?" @@ -115,7 +120,7 @@ This will: 1) Disable the script file - @(path->string (build-path dir file)) + @(lib:directory->pretty-string the-lib dir #:file file)) 2) Create a new 'shadow' script file @(path->string new-script-path) @@ -147,27 +152,25 @@ '(caution ok-cancel))))) (when overwrite? (display-to-file - (make-shadow-script (build-path dir file)) + (let ([pth (build-path dir file)]) + (make-shadow-script pth (lib:path->writable-module-path the-lib pth))) new-script-path #:exists 'replace) (ex/include-selected-file 'exclude) - (dir-lb-select user-script-dir) + (dir-lb-select (user-script-dir)) (when drracket-parent? (send parent-frame open-in-new-tab new-script-path)))))) - (define (dir-lb-select [dir (send dir-lb get-string-selection)]) + (define (dir-lb-select [dir (send dir-lb get-datum-selection)]) (when dir - (set! dir (path-string->string dir)) (set-files-lb dir) - (send dir-lb set-string-selection dir) - (define not-user-script-dir? - (not (path-string=? dir user-script-dir))) - (send bt-dir-remove enable not-user-script-dir?) - (send bt-files-shadow enable not-user-script-dir?))) + (send dir-lb set-datum-selection dir) + (send bt-dir-remove enable (lib:removable-directory? the-lib dir)) + (send bt-files-shadow enable (not (equal? dir (user-script-dir)))))) (define (reload-dir-lb) (send dir-lb clear) - (send dir-lb set (lib:directories the-lib))) + (send dir-lb set (lib:directories the-lib #:sorted? #t))) (define (set-msg-help-string dir file) (when (and dir file) @@ -188,9 +191,11 @@ (define dir-panel (new vertical-panel% [parent panels] [style '(auto-hscroll auto-vscroll)])) - (define dir-lb (new list-box% [parent dir-panel] + (define dir-lb (new data-list-box% [parent dir-panel] [label "Directories"] - [choices (lib:directories the-lib)] + [choices (lib:directories the-lib #:sorted? #t)] + [datum->string (λ (dir) + (lib:directory->pretty-string the-lib dir))] [style '(single vertical-label)] [callback (λ (lb ev) (dir-lb-select))])) @@ -210,9 +215,12 @@ (define files-panel (new vertical-panel% [parent panels] [style '(auto-hscroll auto-vscroll)])) (define files-lb - (new list-box% [parent files-panel] + (new data-list-box% [parent files-panel] [label "Scripts"] [choices '()] + [datum->string + (λ (x) + (check+file->un/checked-file (car x) (path->string (cdr x))))] [style '(extended vertical-label)] [callback (λ (lb ev) @@ -269,7 +277,7 @@ [label "&Close"] [callback (λ (bt ev) (send fr show #f))])) - (dir-lb-select user-script-dir) + (dir-lb-select (user-script-dir)) (send fr show #t))) diff --git a/private/library.rkt b/private/library.rkt new file mode 100644 index 0000000..0fdcf73 --- /dev/null +++ b/private/library.rkt @@ -0,0 +1,421 @@ +#lang racket/base +(require racket/contract + racket/file + racket/path + racket/set + racket/serialize + racket/mutability + framework/preferences + pkg/path + setup/collection-search + setup/collects + setup/getinfo + setup/path-to-relative + "base.rkt") + +(module+ for-test + (provide (contract-out + [test-quickscript-dir + (parameter/c (or/c #f path-string?) + (or/c #f complete-directory-path/c))]))) + +(provide (contract-out + [library? + (-> any/c boolean?)] + [load + (-> library?)] + [save! + (-> library? void?)] + [library=? + (-> library? library? boolean?)] + [directory-path? + (-> path? boolean?)] + [directories + (->* [library?] + [#:sorted? any/c] + (listof complete-directory-path/c))] + [rename library-user-script-dir user-script-dir + (-> library? complete-directory-path/c)] + [directory->enabled+file + (-> library? + complete-directory-path/c + (listof (cons/c boolean? path-element?)))] + [all-enabled-scripts + (-> library? (listof (and/c path? complete-path?)))] + [directory library? path? path? boolean?)] + [directory->pretty-string + (->* [library? path?] + [#:file (or/c #f path-element?)] + immutable-string?)] + [path->writable-module-path + (-> library? + (and/c path? complete-path?) + (and/c (list/c (or/c 'file 'lib) immutable-string?) + module-path?))] + [removable-directory? + (-> library? complete-directory-path/c boolean?)] + [library-has-directory? + (-> library? complete-directory-path/c boolean?)] + [add-directory + (->i #:chaperone + ([lib library?] + [dir complete-directory-path/c]) + #:pre (lib dir) (not (library-has-directory? lib dir)) + [_ library?])] + [remove-directory + (->i #:chaperone + ([lib library?] + [dir complete-directory-path/c]) + #:pre (lib dir) (removable-directory? lib dir) + [_ library?])] + [exclude + (->i #:chaperone + ([lib library?] + [dir complete-directory-path/c] + [_ path-element?]) + #:pre (lib dir) (library-has-directory? lib dir) + [_ library?])] + [include ; TODO: removal of exclusions for absent collections (needs different API: no path) + (->i #:chaperone + ([lib library?] + [dir complete-directory-path/c] + [_ path-element?]) + #:pre (lib dir) (library-has-directory? lib dir) + [_ library?])])) + +;; Conceptually, a library encapsulates: +;; - a set of directories containing script files; and +;; - a set of script files to *not* include (called exclusions). +;; That is, by default all non-excluded files are included (in particular the new ones). +;; +;; The user-script-dir contains ad-hoc scripts shared across Racket versions +;; and is where new scripts are created by the UI. +;; It is part of the library by definition: we store only a set of file names to exclude. +;; +;; The user may add additional ad-hoc directories (also shared across Racket versions), +;; for which we store a hash table mapping complete paths to sets of file names to exclude. +;; More specifically, keys must syntactically specify directories: +;; this uniformity eases comparison, even though it does not solve the +;; general problem of path “equivalence”, which is complex, potentially filesystem-dependent, +;; and not needed in this context. +;; +;; For scripts installed as part of a Racket package---or, more generally, +;; in a collection---the situation is a bit more complicated. +;; These directories are registered declaratively by including +;; `(define quickscript-directory #t)` in an info.rkt file. +;; Observe that the info.rkt file applies to a specific directory: +;; “collection splicing” means that a given collection may have files in +;; multiple directories, none, all, or perhaps only some of which may +;; be Quickscript directories. +;; Furthermore, package authors expect to be able to change which package +;; supplies a particular module as long as they declare appropriate dependencies +;; to maintain compatibility. +;; Therefore: +;; - For display to users, we preserve the distinctions among directories +;; using path->relative-string/library, which includes package information. +;; - For persistent storage, we represent a collection-based exclusion as a +;; normalized-lib-module-path?, which will continue to apply regardless of +;; what package (or even direct collection link) supplies the collection. +;; - The set of collection-based script directories is already stored as part +;; of the Racket installation (in the info.rkt files and caches). +;; We do not store it again with our saved state. + +(define (directory-path? x) + (eq? x (path->directory-path x))) +(define/final-prop path-element-set/c + (set/c path-element? #:cmp 'equal-always)) +(define/final-prop complete-directory-path/c + (and/c path? complete-path? directory-path?)) + +;; the library data we save (shared across Racket versions) +(serializable-struct + ;; can evolve in the future using serializable-struct/versions + library-data (user-exclusions table collection-exclusions) + #:guard (struct-guard/c + path-element-set/c + (and/c (hash/c #:immutable #t + #:flat? #t + complete-directory-path/c + path-element-set/c) + hash-equal-always?) + (set/c (and/c normalized-lib-module-path? + (list/c 'lib immutable-string?)) + #:cmp 'equal-always)) + #:transparent) +(define (empty-library-data) + (library-data (setalw) #hashalw() (setalw))) + +;; a wrapper with installation info and some caches +;; this is NOT thread-safe, due to hash mutation +(struct library (user-script-dir lib collects-script-dirs setup-cache mp-cache pretty-cache) + #:transparent) +(define (library-data->library maybe-lib) + (let* ([quickscript-dir (or (test-quickscript-dir) standard-quickscript-dir)] + [user-script-dir (path->directory-path (build-path quickscript-dir "user-scripts"))] + [lib (or maybe-lib (struct-copy + library-data (empty-library-data) + [user-exclusions (user-exclusions-from-deprecated-library + #:user-script-dir user-script-dir)]))] + [lib (cond + [(hash-ref (library-data-table lib) user-script-dir #f) + (λ (st) + (log-quickscript-error + "saved library data contained user-scripts-dir as an extra directory") + (struct-copy library-data lib + [table (hash-remove (library-data-table lib) user-script-dir)] + [user-exclusions (set-union st (library-data-user-exclusions st))]))] + [else + lib])] + [setup-cache (make-hash)] + [collects-script-dirs (find-collection-based-script-directories)] + [collects-scripts-dirs + (if (test-quickscript-dir) + (for/setalw ([dir (in-immutable-set collects-script-dirs)] + #:when (equal? "quickscript" (path->pkg dir #:cache setup-cache))) + dir) + collects-script-dirs)]) + (library user-script-dir + lib + collects-scripts-dirs + setup-cache + (make-hash) + (make-hash)))) + +(define standard-quickscript-dir + ;; not guaranteed to exist + (build-path (find-system-path 'pref-dir) "quickscript")) +(define test-quickscript-dir + ;; #f means we are not currently testing, so use standard-quickscript-dir + ;; When non-false, this parameter also arranges for (load) to ignore + ;; collection-based scripts that do not come from the "quickscript" package. + (make-parameter #f (λ (x) + (and x (path->directory-path (path->complete-path x)))))) + +(define find-collection-based-script-directories + (let ([absent (gensym)]) + (define (find-collection-based-script-directories) + (for*/setalw ([dir (find-relevant-directories '(quickscript-directory))] + [info (in-value (get-info/full dir))] + #:when info + [v (in-value (info 'quickscript-directory (λ () absent)))] + #:when (cond + [(eq? #t v)] + [else + (unless (eq? absent v) + (log-quickscript-error + "~a\n expected: ~e\n given: ~e\n directory: ~e" + "bad value for quickscript-directory in info file" + #t + v + dir)) + #f])) + (path->directory-path dir))) + find-collection-based-script-directories)) + +(define (user-exclusions-from-deprecated-library #:user-script-dir user-script-dir) + (define deprecated-library-file + (build-path user-script-dir 'up "library.rktd")) + (or (and (file-exists? deprecated-library-file) + (with-handlers ([exn:fail? (λ (e) + (log-quickscript-error "error importing from ~e: ~v" + deprecated-library-file + (exn-message e)) + #f)]) + (for/first ([{dir lst} (in-hash (file->value deprecated-library-file))] + #:when (equal? user-script-dir + (path->complete-path (path->directory-path dir)))) + (for/setalw ([s (in-list lst)]) + (string->path-element s))))) + (setalw))) + +;; library-data is stored using the framework/preferences system, +;; which provides help for future changes without breaking compatibility +;; Calling (default-library-data) may consult the filesystem, so do it +;; during (load), not when instantiating this module. +(define pref-key 'plt:quickscript:library) +(preferences:set-default pref-key #f (or/c library-data? #f)) +(preferences:set-un/marshall pref-key + (λ (x) + (with-handlers ([exn:fail? (λ (e) 'corrupt)]) + (serialize x))) + (λ (x) + (with-handlers ([exn:fail? void]) + (deserialize x)))) +(define (load) + (library-data->library (preferences:get pref-key))) +(define (save! lib) + (preferences:set pref-key (library-lib lib))) + +(define (library=? a b) + ;; ignores caches + (define-syntax-rule (cf fld ...) + (and (equal-always? (fld a) (fld b)) ...)) + (cf library-user-script-dir + library-lib + library-collects-script-dirs)) + +(define (directorypretty-string + ;; - unknown paths sorted by pathpkg a #:cache cache)] + [b-pkg (path->pkg b #:cache cache)]) + (cond + [(equal? a-pkg b-pkg) + (stringpretty-string lib a) + (directory->pretty-string lib b))] + [(and a-pkg b-pkg) + (stringpretty-string lib dir #:file [file #f]) + (define full + (if file + (build-path dir file) + dir)) + (hash-ref! + (library-pretty-cache lib) + full + (λ () + (string->immutable-string + (if (hash-has-key? (library-data-table (library-lib lib)) dir) + (path->string full) + (path->relative-string/library full #:cache (library-setup-cache lib))))))) + +(define (path->normalized-lib-module-path lib pth) + (hash-ref! + (library-mp-cache lib) + pth + (λ () + (define rslt + (path->module-path pth #:cache (library-setup-cache lib))) + (and (normalized-lib-module-path? rslt) + `(lib ,(string->immutable-string (cadr rslt))))))) + +(define (path->writable-module-path lib pth) + ;; TODO: maybe "read" able, since we mean that we can `write` the path, not write TO the path + (or (path->normalized-lib-module-path lib pth) + `(file ,(string->immutable-string (path->string pth))))) + +(define (directories lib #:sorted? [sorted? #f]) + (cond + [sorted? + (sort (directories lib #:sorted? #f) + (λ (a b) + (directorylist (library-collects-script-dirs lib))))])) + +(define (directory->enabled+file lib dir) + (define data (library-lib lib)) + (define enabled? + (cond + [(if (equal? dir (library-user-script-dir lib)) + (library-data-user-exclusions data) + (hash-ref (library-data-table data) dir #f)) + => (λ (excludes) + (λ (name) + (not (set-member? excludes name))))] + [else + (define excludes (library-data-collection-exclusions data)) + (λ (name) + (define pth (build-path dir name)) + (not (set-member? excludes (path->normalized-lib-module-path lib pth))))])) + (for/list ([name (in-list (if (directory-exists? dir) + (directory-list dir #:build? #f) + '()))] + #:when (and (script-file? name) + (file-exists? (build-path dir name)))) ; i.e., not a directory + (cons (enabled? name) name))) + +(define (all-enabled-scripts lib) + (for*/list ([dir (in-list (directories lib))] + [enabled+file (in-list (directory->enabled+file lib dir))] + #:when (car enabled+file)) + (build-path dir (cdr enabled+file)))) + +(define (removable-directory? lib dir) + (hash-has-key? (library-data-table (library-lib lib)) dir)) + +(define (library-has-directory? lib dir) + (or (equal? dir (library-user-script-dir lib)) + (removable-directory? lib dir) + (set-member? (library-collects-script-dirs lib) dir))) + +(define (add-directory lib dir) + (define data (library-lib lib)) + (struct-copy library lib + [lib (struct-copy library-data data + [table (hash-update (library-data-table data) + dir + values + setalw)])])) + +(define (remove-directory lib dir) + (define data (library-lib lib)) + (struct-copy library lib + [lib (struct-copy library-data data + [table (hash-remove (library-data-table data) + dir)])])) + +(define (in/exclude set-change lib dir filename) + (define data (library-lib lib)) + (struct-copy library lib + [lib (cond + [(equal? dir (library-user-script-dir lib)) + (struct-copy + library-data data + [user-exclusions (set-change (library-data-user-exclusions data) filename)])] + [(set-member? (library-collects-script-dirs lib) dir) + (struct-copy + library-data data + [collection-exclusions + (set-change (library-data-collection-exclusions data) + (path->normalized-lib-module-path lib (build-path dir filename)))])] + [else + (struct-copy + library-data data + [table (hash-update (library-data-table data) + dir + (λ (excludes) + (set-change excludes filename)))])])])) + +(define (exclude lib dir filename) + (in/exclude set-add lib dir filename)) + +(define (include lib dir filename) + (in/exclude set-remove lib dir filename)) diff --git a/shadow-script.rkt b/private/shadow-script.rkt similarity index 89% rename from shadow-script.rkt rename to private/shadow-script.rkt index a97b484..0eba2dc 100644 --- a/shadow-script.rkt +++ b/private/shadow-script.rkt @@ -8,11 +8,11 @@ (define shadow-prefix "shadow:") -(define (make-header f) +(define (make-header writable-module-path) @string-append{ #lang racket/base (require quickscript - (prefix-in @shadow-prefix (file @(~s (path->string f))))) + (prefix-in @shadow-prefix @(~s writable-module-path))) ;;; This is a 'shadow' script. ;;; The script functions below call the functions of the original script, @@ -41,12 +41,12 @@ }) -(define (make-shadow-script f) +(define (make-shadow-script f writable-module-path) (parameterize ([current-namespace (make-base-empty-namespace)]) (define props-dict (get-property-dicts f)) (define funs (dict-keys props-dict)) (string-append - (make-header f) + (make-header writable-module-path) "\n" (string-join (for/list ([props (in-list props-dict)]) @@ -74,4 +74,4 @@ (define qs-path (resolve-module-path 'quickscript-extra)) (define f (build-path (path-only qs-path) "scripts" "bookmarks.rkt")) - (displayln (make-shadow-script f)))) + (displayln (make-shadow-script f #|FIXME|#)))) diff --git a/scribblings/quickscript.scrbl b/scribblings/quickscript.scrbl index af61c2d..4b873c4 100644 --- a/scribblings/quickscript.scrbl +++ b/scribblings/quickscript.scrbl @@ -446,33 +446,15 @@ or on @hyperlink["http://pasterack.org/"]{PasteRack}, and share the link. A user can then copy/paste the contents into a new script. Don't forget to include a permissive license such as MIT/Apache 2. - -The @emph{best} way to distribute scripts is by creating a package---the user only has to install -the package. -Assuming your scripts are stored in the @racket["scripts"] subdirectory, -include a file (say @racket["register.rkt"]) at the root directory of -the package containing the following code: -@margin-note{If the file @racket["register.rkt"] is not at the root, - the runtime-path needs to be modified accordingly.} -@codeblock|{ -#lang racket/base -(require (for-syntax racket/base - racket/runtime-path - (only-in quickscript/library - add-third-party-script-directory!))) - -;; This file is going to be called during setup and will automatically -;; register the scripts subdirectory in quickscript's library. -(begin-for-syntax - (define-runtime-path script-dir "scripts") - (add-third-party-script-directory! script-dir)) - }| - -You can see an example with -@hyperlink["https://github.com/Metaxal/quickscript-extra"]{quickscript-extra}. - -Don't forget to register your package on the -@hyperlink["https://pkgs.racket-lang.org/"]{Racket server}. +@; The @emph{best} way to distribute scripts is by creating a package---the user only has to install +@; the package. +@; ... but the current mechanism is broken, see https://github.com/Metaxal/quickscript/issues/79 ... +@; +@; You can see an example with +@; @hyperlink["https://github.com/Metaxal/quickscript-extra"]{quickscript-extra}. +@; +@; Don't forget to register your package on the +@; @hyperlink["https://pkgs.racket-lang.org/"]{Racket server}. @section{License} diff --git a/scripts/eyes.rkt b/scripts/eyes.rkt new file mode 100644 index 0000000..89a24b1 --- /dev/null +++ b/scripts/eyes.rkt @@ -0,0 +1,93 @@ +#lang racket/gui +(require quickscript) + +;;; Author: Stephen De Gabrielle https://github.com/spdegabrielle +;;; License: [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) or +;;; [MIT license](http://opensource.org/licenses/MIT) at your option. +;;; From: https://github.com/Quickscript-Competiton/July2020entries/issues/7 + +(script-help-string "Eyeballs are following you.") + +(define (eye-canvas-mixin %) + (class % + (init-field (eye-diameter 100)) + (inherit refresh get-dc client->screen screen->client get-top-level-window) + (define pupil-diameter (/ eye-diameter 3)) + (define pupil-r (* 1/2 pupil-diameter)) + (define r (/ eye-diameter 2)) + + (define/override (on-paint) + ;save the state + (define dc (get-dc)) + (define pen (send dc get-pen)) + (define brush (send dc get-brush)) + (define f (get-top-level-window)) + ;; now draw the eye + (send dc set-pen "black" 1 'solid) + (send dc set-brush "white" 'solid) + (send dc draw-ellipse 0 0 eye-diameter eye-diameter) + + ;As for the magic number, the difference between + ;get-current-mouse-state and client->screen may be + ;get-display-left-top-inset. + + (define-values (not-used-x fsy) (get-display-left-top-inset)) + + + (define-values (ms l) (get-current-mouse-state)) + (define mouse-sx (round (send ms get-x))) ; screen coords + (define mouse-sy (+ fsy (round (send ms get-y)))) + (define-values (mcx mcy) (send this screen->client mouse-sx mouse-sy)) + (define-values (screen-eye-x screen-eye-y) (client->screen r r)) + (define Δx (- screen-eye-x mouse-sx)) + (define Δy (- screen-eye-y mouse-sy)) + (define mag (magnitude (make-rectangular Δx Δy))) + + (if (< mag (- r pupil-r)) + (begin + (send dc set-brush "black" 'solid) + (send dc draw-ellipse (- mcx pupil-r) (- mcy pupil-r) pupil-diameter pupil-diameter)) + (let ((direction (atan Δy Δx))) + (define pupilΔx (- (round (* (cos direction) (* r 2/3))))) + (define pupilΔy (- (round (* (sin direction) (* r 2/3))))) + (define (tocentre n) (- (+ r n) pupil-r)) + (define px (tocentre pupilΔx)) + (define py (tocentre pupilΔy)) + (send dc set-brush "black" 'solid) + (send dc draw-ellipse px py pupil-diameter pupil-diameter))) + (send dc set-pen pen) + (send dc set-brush brush) + (super on-paint)) + (super-new [style '(transparent)]))) + + +(define-script eyes + #:label "Eyes" + #:menu-path ("&Games and fun") + #:help-string "Eyeballs are following you." + #:persistent + (λ (selection) + + (define frame (new frame% [label "Eyes"] [width 80] [height 90])) + (define h (new horizontal-panel% [parent frame])) + (define c (new (eye-canvas-mixin canvas%) [parent h](eye-diameter 40))) + (define c2 (new (eye-canvas-mixin canvas%) [parent h](eye-diameter 40))) + (send frame show #t) + + (define t (new timer% + [notify-callback (λ () (send c refresh)(send c2 refresh))] + [interval 100] + [just-once? #f])) + #f)) + +(module+ main + (define frame (new frame% [label "Eyes"] [width 80] [height 90])) + (define h (new horizontal-panel% [parent frame])) + (define c (new (eye-canvas-mixin canvas%) [parent h](eye-diameter 40))) + (define c2 (new (eye-canvas-mixin canvas%) [parent h](eye-diameter 40))) + (send frame show #t) + + (define t (new timer% + [notify-callback (λ () (send c refresh)(send c2 refresh))] + [interval 100] + [just-once? #f]))) diff --git a/scripts/info.rkt b/scripts/info.rkt new file mode 100644 index 0000000..aaacb35 --- /dev/null +++ b/scripts/info.rkt @@ -0,0 +1,2 @@ +#lang info +(define quickscript-directory #t) diff --git a/scripts/open-terminal.rkt b/scripts/open-terminal.rkt new file mode 100644 index 0000000..6b57233 --- /dev/null +++ b/scripts/open-terminal.rkt @@ -0,0 +1,27 @@ +#lang racket/base +(require racket/system + racket/path + quickscript) + +(script-help-string "Open a terminal in the directory of the current file.") + +(define-script open-terminal + #:label "Open terminal here" + #:menu-path ("&Utils") + #:os-types (unix macosx windows) + (λ (str #:file f) + (unless f + (set! f (current-directory))) + (define dir (path->string (path-only f))) + (case (system-type 'os) + [(unix) + (system (string-append "gnome-terminal" + " --working-directory=\"" dir "\"" + " -t \"" dir "\"" + "&"))] + [(macosx) + (system + (string-append "osascript -e 'tell app \"Terminal\" to do script \"cd \\\"" dir "\\\"\"'" ))] + [(windows) + (shell-execute #f "cmd.exe" "" dir 'sw_shownormal)]) + #false)) diff --git a/tests/library.rkt b/tests/library.rkt index 73d9570..fd4529a 100644 --- a/tests/library.rkt +++ b/tests/library.rkt @@ -1,69 +1,202 @@ #lang racket -;; Tests n a separate file to test the contracts too. +;; Tests in a separate file to test the contracts too. (require rackunit - quickscript/base - quickscript/library) - -(define my-lib (new-library)) -(check set=? - (directories my-lib) - (map path-string->string (list user-script-dir))) - -(define dummy-dir (build-path (find-system-path 'temp-dir) - "dummy-script-dir")) -; Make sure we control what the directory contains. -(make-directory* dummy-dir) -(for-each delete-file (directory-list dummy-dir #:build? #t)) - -(define lib-path (build-path dummy-dir "library.rktd")) - -(set! my-lib (load lib-path)) - -(add-directory! my-lib dummy-dir) -(check set=? - (directories my-lib) - (map path-string->string (list user-script-dir dummy-dir))) - - -(define script1 "script1.rkt") -(define script2 "script2.rkt") -(define not-a-script "script.notrkt") -(display-to-file "\n" (build-path dummy-dir script1)) -(display-to-file "\n" (build-path dummy-dir script2)) -(display-to-file "\n" (build-path dummy-dir not-a-script)) -(check set=? - (files my-lib dummy-dir) - (list script1 script2)) - -(exclude! my-lib dummy-dir script1) -(check set=? - (files my-lib dummy-dir) - (list script2)) -(exclude! my-lib dummy-dir script2) -(check set=? - (files my-lib dummy-dir) - '()) - -(include! my-lib dummy-dir script1) -(check set=? - (files my-lib dummy-dir) - (list script1)) - -(remove-directory! my-lib dummy-dir) -(check set=? - (directories my-lib) - (map path-string->string (list user-script-dir))) - -;; Check load and save! -(add-directory! my-lib dummy-dir) -(exclude! my-lib dummy-dir script2) -(define my-lib-file (build-path dummy-dir "my-lib.rktd")) -(save! my-lib my-lib-file) -(define my-lib2 (load my-lib-file)) -(check set=? (dict-keys my-lib) (dict-keys my-lib2)) -(for ([(dir excl-list) (in-dict my-lib)]) - (check-equal? excl-list (exclusions my-lib2 dir))) -(check set=? - (all-files my-lib) - (all-files my-lib2)) -#;(all-files my-lib2) + "../private/base.rkt" + "../private/library.rkt" + (submod "../private/library.rkt" for-test) + (only-in pkg/lib pkg-directory) + (only-in setup/getinfo reset-relevant-directories-state!) + framework/preferences) + +(define-binary-check (check-equal-always? equal-always? actual expected)) +(define-binary-check (check-library=? library=? actual expected)) +(define-simple-check (check-not-library=? v1 v2) + (not (library=? v1 v2))) + +(define (pe s) + (string->path-element s)) + +(define (touch* #:in dir . files) + (make-directory* dir) + (for ([f (in-list files)]) + (call-with-output-file* (build-path dir f) void))) + +(define prefs-table + (make-hash)) + +(define tmp-qs-dir + (path->directory-path (make-temporary-directory))) + +(define-syntax-rule (in-test-context body ...) + (parameterize ([test-quickscript-dir tmp-qs-dir] + [preferences:low-level-get-preference + (λ (name [fail (λ () #f)]) + (hash-ref prefs-table name fail))] + [preferences:low-level-put-preferences + (λ (names vals) + (for ([name (in-list names)] + [val (in-list vals)]) + (hash-set! prefs-table name val)))]) + (preferences:restore-defaults) + (reset-relevant-directories-state!) + (dynamic-wind + void + (λ () + (call-with-continuation-barrier + (λ () + body ...))) + (λ () + (delete-directory/files tmp-qs-dir #:must-exist? #f))))) + +(in-test-context + + (define the-user-script-dir + (build-path tmp-qs-dir "user-scripts/")) + + (define quickscript-pkg-directory + (cond + [(pkg-directory "quickscript") + => simplify-path] + [else + (fail-check "tests require the \"quickscript\" package to be installed")])) + (define the-pkg-script-dir + (build-path quickscript-pkg-directory "scripts/")) + (define expected-pkg-enabled+file + `([#t . ,(pe "eyes.rkt")] + [#t . ,(pe "open-terminal.rkt")])) + + (define my-lib (load)) + + (check-equal? (user-script-dir my-lib) + the-user-script-dir + "user-script-dir configured for testing") + + (check-equal-always? (directories my-lib) + (list the-user-script-dir + the-pkg-script-dir) + "default library directories") + + (check-equal-always? (directory->enabled+file my-lib the-user-script-dir) + '() + "no user script yet") + + (check-equal-always? (directory->enabled+file my-lib the-pkg-script-dir) + expected-pkg-enabled+file + "expected package scripts") + + (define extra-dir + (build-path tmp-qs-dir "extra/")) + + (define lib+extra + (add-directory my-lib extra-dir)) + + (check-equal-always? (directories lib+extra) + (list the-user-script-dir + extra-dir + the-pkg-script-dir) + "add-directory") + + (check-equal-always? (directory->enabled+file lib+extra extra-dir) + '() + "extra-dir does not exist yet") + + (define script1 (pe "script1.rkt")) + (define script2 (pe "script2.rkt")) + (define not-a-script (pe "script3.notrkt")) + (touch* #:in extra-dir script1 script2 not-a-script) + + (check-equal-always? (directory->enabled+file lib+extra extra-dir) + `([#t . ,script1] + [#t . ,script2]) + "correct scripts in extra directory") + + (define lib+excludes + (for/fold ([lib lib+extra]) + ([d (list extra-dir + the-pkg-script-dir + the-user-script-dir)] + [f (list script1 + (cdar expected-pkg-enabled+file) + (pe "excluded.rkt"))]) ; does not exist yet + (exclude lib d f))) + + (check-equal-always? (directory->enabled+file lib+excludes the-pkg-script-dir) + `([#f . ,(cdar expected-pkg-enabled+file)] + ,@(cdr expected-pkg-enabled+file)) + "package script exclusions") + + (check-equal-always? (directory->enabled+file + (include lib+excludes + the-pkg-script-dir + (cdar expected-pkg-enabled+file)) + the-pkg-script-dir) + expected-pkg-enabled+file + "include for package script") + + (check-equal-always? (directory->enabled+file lib+excludes extra-dir) + `([#f . ,script1] + [#t . ,script2]) + "extra-dir excludes") + + (check-equal-always? (directory->enabled+file + (include (exclude lib+excludes extra-dir script2) + extra-dir + script1) + extra-dir) + `([#t . ,script1] + [#f . ,script2]) + "extra-dir: swap includes and excludes") + + (define extra+pkg-enabled-scripts + (cons (build-path extra-dir script2) + (for/list ([pr (in-list (cdr expected-pkg-enabled+file))]) + (build-path the-pkg-script-dir (cdr pr))))) + + (check-equal-always? (all-enabled-scripts lib+excludes) + extra+pkg-enabled-scripts + "expected enabled scripts before user") + + (touch* #:in the-user-script-dir "user.rkt" "excluded.rkt") + + (check-equal-always? (all-enabled-scripts lib+excludes) + (cons (build-path the-user-script-dir "user.rkt") + extra+pkg-enabled-scripts) + "should detect added user script") + + (check-equal-always? (directory->enabled+file lib+excludes the-user-script-dir) + `([#f . ,(pe "excluded.rkt")] + [#t . ,(pe "user.rkt")]) + "should recognize pre-excluded user script") + + (check-equal-always? (directory->enabled+file + (include lib+excludes the-user-script-dir (pe "excluded.rkt")) + the-user-script-dir) + `([#t . ,(pe "excluded.rkt")] + [#t . ,(pe "user.rkt")]) + "include user script") + + (delete-file (build-path the-user-script-dir "user.rkt")) + + (check-equal-always? (all-enabled-scripts lib+excludes) + extra+pkg-enabled-scripts + "should detect deleted user script") + + (check-equal-always? (directories + (remove-directory lib+excludes extra-dir)) + (list the-user-script-dir + the-pkg-script-dir) + "can remove extra-dir") + + ;; TODO: test correct errors from add remove include exclude + + ;; Check load and save! + (check-not-library=? my-lib + lib+excludes + "my-lib is different than lib+excludes") + (check-library=? (load) + my-lib + "(load) reproduces my-lib") + (save! lib+excludes) + (check-library=? (load) + lib+excludes + "save and restore lib+excludes")) diff --git a/tool.rkt b/tool.rkt index e18463d..1917626 100644 --- a/tool.rkt +++ b/tool.rkt @@ -2,7 +2,7 @@ (require (for-syntax racket/base) ; for help menu drracket/tool ; necessary to build a drracket plugin - framework ; for preferences (too heavy a package?) + framework help/search net/sendurl ; for the help menu racket/class @@ -12,10 +12,11 @@ racket/list racket/string racket/unit - "base.rkt" - "exn-gobbler.rkt" - (prefix-in lib: "library.rkt") - "library-gui.rkt") + setup/getinfo + "private/base.rkt" + "private/exn-gobbler.rkt" + (prefix-in lib: "private/library.rkt") + "private/library-gui.rkt") (provide tool@) #| @@ -32,9 +33,6 @@ The maximize button of the frame also disappears, as if the X11 maximize propert (define orig-display-handler #f) ; will be set in the unit. -(define (user-script-files #:exclude? [exclude? #t]) - (lib:all-files (lib:load library-file) #:exclude? exclude?)) - (define (error-message-box str e) (define sp (open-output-string)) (parameterize ([current-error-port sp]) @@ -64,12 +62,14 @@ The maximize button of the frame also disappears, as if the X11 maximize propert '(caution ok)))) ;; -> exn-gobbler? +#; (define (compile-library) (time-info "Recompiling library" (parameterize ([error-display-handler orig-display-handler]) (compile-user-scripts (user-script-files))))) ;; -> void? +#; (define (compile-library/frame) (define fr #false) (dynamic-wind @@ -125,11 +125,18 @@ The maximize button of the frame also disappears, as if the X11 maximize propert (get-interactions-text))) (define/private (new-script) + (define (name-ok? name) + (and (non-empty-string? name) + (string->path-element name 'false-on-non-element) + (not (string-ci=? name "info")))) (define name (get-text-from-user "Script name" "Enter the name of the new script:" this - #:validate non-empty-string? + #f + "" + '(disallow-invalid) + #:validate name-ok? #:dialog-mixin frame:focus-table-mixin)) - (when name + (when (and name (name-ok? name)) (define filename (string-append (string-foldcase (string-replace name " " "-")) ".rkt")) (define file-path (build-path user-script-dir filename)) (define proc-name (string-foldcase (string-replace name " " "-"))) @@ -232,7 +239,7 @@ The maximize button of the frame also disappears, as if the X11 maximize propert ; See HelpDesk for "Manipulating namespaces" (let ([f (parameterize ([current-namespace ns]) ; Ensure the script is compiled for the correct version of Racket - (compile-user-script fpath) + #;(compile-user-script fpath) (dynamic-require fpath name))] [kw-dict (append @@ -344,6 +351,7 @@ The maximize button of the frame also disappears, as if the X11 maximize propert ;; All menu item scripts have are at the key `#f` in property-dicts. ;; The key for other scripts (hooks, not menu entries) is the script's identifier (name). (define property-dicts (make-hasheq)) + (define user-script-dir 'user-script-dir-not-loaded) (define/private (load-properties!) (set! property-dicts (make-hasheq)) @@ -351,8 +359,10 @@ The maximize button of the frame also disappears, as if the X11 maximize propert ;; Create an empty namespace to load all the scripts (in the same namespace). (parameterize ([current-namespace (make-base-empty-namespace)] [error-display-handler orig-display-handler]) + (define lib (lib:load)) + (set! user-script-dir (lib:user-script-dir lib)) ;; For all script files in the script directory. - (for ([f (in-list (user-script-files))]) + (for ([f (in-list (lib:all-enabled-scripts lib))]) (time-info (string-append "Loading file " (path->string f)) (with-handlers* ([exn:fail? @@ -379,6 +389,9 @@ The maximize button of the frame also disappears, as if the X11 maximize propert (set! menu-reload-count (add1 menu-reload-count)) (log-quickscript-info "Script menu rebuild #~a..." menu-reload-count) + (unless (eq? user-script-dir 'user-script-dir-not-loaded) + (reset-relevant-directories-state!)) + (load-properties!) (let* ([property-dicts @@ -447,6 +460,7 @@ The maximize button of the frame also disappears, as if the X11 maximize propert ("&Reload menu" . ,(λ () (unload-persistent-scripts) (reload-scripts-menu))) + #; ("&Compile scripts" . ,(λ () (unload-persistent-scripts) (compile-library/frame) @@ -463,7 +477,7 @@ The maximize button of the frame also disappears, as if the X11 maximize propert (new separator-menu-item% [parent scripts-menu]) ;; Show the error messages that happened during the initial compilation. - (exn-gobbler-message-box init-compile-exn-gobbler "Quickscript: Error during compilation") + #;(exn-gobbler-message-box init-compile-exn-gobbler "Quickscript: Error during compilation") (reload-scripts-menu) (on-startup))) @@ -559,7 +573,7 @@ The maximize button of the frame also disappears, as if the X11 maximize propert ; This must be done before building the menus. ; The compilation is done at this point so that the splash screen doesn't disappear, ; but the message box will be shown after the DrRacket frame is shown up. - (define init-compile-exn-gobbler (compile-library)) + #;(define init-compile-exn-gobbler (compile-library)) ;; Search for "Extending the Existing DrRacket Classes" to see what can be extended: (drracket:get/extend:extend-definitions-text text-mixin)