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
|
;; Copyright (c) 2020-2023 by Greg Hendershott.
;; SPDX-License-Identifier: GPL-3.0-or-later
#lang racket/base
(require racket/async-channel
racket/class
racket/contract/base
racket/contract/option
racket/format
racket/match
syntax-color/token-tree
syntax-color/paren-tree
(only-in syntax-color/lexer-contract dont-stop)
(only-in syntax-color/color-textoid color-textoid<%>)
(only-in syntax-color/module-lexer module-lexer*)
(only-in syntax-color/racket-lexer racket-lexer)
(only-in syntax-color/racket-indentation racket-amount-to-indent)
(only-in syntax-color/racket-navigation racket-grouping-position)
syntax/parse/define
"lang-info.rkt"
(prefix-in lines: "text-lines.rkt")
"util.rkt")
(provide hash-lang%
generation/c
position/c
min-position
(struct-out lang-info))
;; Overview
;;
;; An instance of a hash-lang% object can be used to represent program
;; source text and obtain information based on the #lang.
;;
;; The hash-lang% `update!` method may be called safely from any
;; thread to change the program source text (e.g. as the result of a
;; human editing the text). The `update!` method returns immediately;
;; the actual updating work is handled by a dedicated thread.
;; Furthermore the updater minimizes the work done for a change. As a
;; result it is fine to call `update!` frequently for edits that
;; insert or delete a single character, as well as for bigger changes.
;;
;; Each update! must specify a "generation", which is a strictly
;; successive increasing exact integer. A new object is generation 0;
;; the first update! must be generation 1. [It is fine if update!
;; calls are made from multiple threads and arrive with out-of-order
;; generation numbers; they are automatically queued and handled in
;; the correct order.]
;;
;; Other public methods -- `classify`, `get-tokens`, `grouping`,
;; `indent-line`, `indent-range` -- take both a generation and a
;; position. They automatically block until the updating thread has
;; progressed through that generation and position.
;;
;; The generation number is intended to support "distributed" use
;; patterns, where the editor might live in a different process or
;; even on a remote machine.
;;
;; As the updater thread works, it may produce "notifications" by
;; calling the `on-changed-lang-info` and `on-changed-token` methods.
;; This happens on the updater thread; the recipient should only queue
;; these (e.g. in an async channel) to handle later in some other
;; thread, and return immediately.
;;
;; `on-changed-lang-info` is called for the generation 1 update, as
;; well as for updates that change the #lang meaningfully (change lang
;; info values such as 'color-lexer or 'drracket:indentation).
;;
;; `on-changed-tokens` is called when an update! results in different
;; tokens for some span. The recipient should simply queue this
;; information in an async channel. What should it do when retrieving
;; them later? It depends on the program. One approach is to call
;; `get-tokens` eagerly for the entire invalid span and use the tokens
;; to color/propertize the entire span. Another approach is to record
;; the invalid span, but let some other mechanism call `get-tokens`
;; only if/as/when portions of the invalid span become visible to the
;; user, such as when they scroll. (The latter approach is what we use
;; in Emacs: Clear a "fontified" property for the invalid region, and
;; let the normal font-lock mechanism ask us to fontify visible
;; non-fontified areas.)
;;
;; Although this class implements the color-textoid<%> interface,
;; those methods are NOT intended to be used directly by a tool ---
;; for speed they are intentionally NOT thread-safe! Instead the
;; `grouping` and `indent-x` methods work by supplying these methods
;; to a lang grouper or indenter, within a single dynamic extent where
;; it is thread-safe to call them.
;;
;;
;; Portions originated from
;; /src/racket-lang/racket/share/pkgs/gui-lib/framework/private
(define generation/c exact-nonnegative-integer?)
;; We use 0-based positions
(define min-position 0)
(define max-position (sub1 (expt 2 63)))
(define position/c (integer-in min-position max-position))
;; Our data for token-tree%
(struct data (attribs backup mode) #:transparent #:authentic)
(define-simple-macro (with-semaphore sema e:expr ...+)
(call-with-semaphore sema (λ () e ...)))
(define hash-lang%
(class* object% (color-textoid<%>)
(super-new)
;; Virtual methods to override for notifications
(define/public (on-changed-lang-info gen li) (void))
(define/public (on-changed-tokens gen beg end) (void))
;; A new object has an empty string and is at updated-generation
;; 0. The creator should then use update! to set the initial
;; string value and start the initial tokenization. That way both
;; `new` and `update!` return immediately, and all tokenization is
;; done on the updater thread.
(define updated-generation 0)
(define updated-position (sub1 min-position))
(define content lines:empty-text-lines)
(define tokens (new token-tree%))
(define tokens-sema (make-semaphore 1))
(define parens (new paren-tree% [matches default-paren-matches]))
(define parens-sema (make-semaphore 1))
;; By default the lang is read from `content`, for when that
;; represents a source file containing #lang or a file module.
;; However `other-lang-source` may be a string used instead to
;; read the language, for a REPL buffer that should use the lang
;; from the file for which it is a REPL.
(init-field [other-lang-source #f])
(define lang-info (if other-lang-source
(read-lang-info (open-input-string other-lang-source))
default-lang-info))
(define/public (get-lang-info) lang-info)
;; Some methods intended just for tests
(define/public (-get-content) (lines:get-text content 0))
(define/public (-get-modes)
(define modes null)
(send tokens search-min!)
(send tokens
for-each
(λ (beg end data)
(set! modes (cons (list beg end (data-mode data))
modes))))
(reverse modes))
#;
(define/private (-show-tree msg t [offset 0])
(displayln msg)
(send t for-each
(λ (-beg len dat)
(define beg (+ -beg offset))
(define end (+ beg len))
(println (vector beg end (lines:get-text content beg end) dat)))))
;; position/c -> (or/c #f (list/c position/c position/c token?))
;;
;; Note: To be thread-safe must use tokens-sema.
(define/private (token-ref pos)
(send tokens search! pos)
(define beg (send tokens get-root-start-position))
(define end (send tokens get-root-end-position))
(and (<= beg pos) (< pos end)
(list beg end (send tokens get-root-data))))
;; ----------------------------------------------------------------------
;;
;; Coordinate progress of tokenizing updater thread
;; Allow threads to wait -- safely and without polling -- for the
;; updater thread to progress to at least a given generation and
;; position.
(define monitor (make-monitor))
;; Called from updater thread.
(define/private (set-update-progress #:generation [g updated-generation]
#:position p)
(progress monitor
(λ ()
(set! updated-generation g)
(set! updated-position p))))
;; Called from threads that need to wait for update progress to a
;; certain generation and position.
(define/public (block-until-updated-thru gen [pos max-position])
(wait monitor
(λ ()
(and (<= gen updated-generation)
(<= pos updated-position)))))
;; -----------------------------------------------------------------
;;
;; Tokenizer updater thread
;; Entry thunk of our updater thread, which gets items from the
;; async channel `update-chan`, put there by the public `update!`
;; method.
;;
;; The only complexity here is that we tolerate update requests
;; arriving with out-of-order generation numbers. (This could
;; result from update! being called from various threads. For
;; example Racket Mode commands are each handled on their own
;; thread, much like a web server. As a rough analogy, this is
;; like handling TCP packets arriving possibly out of order.)
;;
;; TODO: Does this complexity belong here in this class, or should
;; it move outside? Strictly speaking this is about coordinating
;; multi-thread calls to our public update! method -- not about
;; coordinating our updater thread with other threads. This could
;; as easily live in e.g. hash-lang-bridge.rkt instead of here.
(define update-chan (make-async-channel))
(thread
(λ ()
(define pending-updates (make-hash))
(let get ([next-update-gen 1])
(match-define (cons gen more) (async-channel-get update-chan))
(hash-set! pending-updates gen more)
(let do-pending ([next-update-gen next-update-gen])
(match (hash-ref pending-updates next-update-gen #f)
[(list pos old-len new-str)
(hash-remove! pending-updates next-update-gen)
(do-update! next-update-gen pos old-len new-str)
(do-pending (add1 next-update-gen))]
[#f (get next-update-gen)])))))
;; Runs on updater thread.
(define/private (do-update! gen pos old-len new-str)
(define new-len (string-length new-str))
;; Initial progress for other threads: Nothing yet within this
;; new generation.
(set-update-progress #:generation gen
#:position (sub1 min-position))
;; Update the text-lines data structure.
(when (< 0 old-len)
(set! content (lines:delete content pos (+ pos old-len))))
(when (< 0 new-len)
(set! content (lines:insert content pos new-str)))
;; Update tokens and parens trees. If lang lexer changed, it
;; could result in entirely different tokens and parens, so in
;; that case restart from scratch.
(cond [(check-lang-info/lexer-changed? gen pos)
(set! tokens (new token-tree%))
(set! parens (new paren-tree%
[matches (lang-info-paren-matches lang-info)]))
(update-tokens-and-parens min-position
(lines:text-length content))]
[else
(update-tokens-and-parens pos
(- new-len old-len))]))
;; Detect whether #lang changed AND ALSO (to avoid excessive
;; notifications and work) whether that changed any lang info
;; values we use. Notify if any changed, or if this is the first
;; generation. Return true IFF the lexer changed. For example this
;; will return false for a change from #lang racket to
;; racket/base.
(define last-lang-end-pos (add1 min-position))
(define/private (check-lang-info/lexer-changed? gen pos)
(define new-lang-info
(cond
[other-lang-source lang-info]
[else
(cond
[(< pos last-lang-end-pos)
(define in (lines:open-input-text content 0))
(define-values (new-lang-info end-pos) (read-lang-info* in))
(set! last-lang-end-pos end-pos) ;for checking next time
new-lang-info]
[else lang-info])]))
(define any-changed? (not (equal? lang-info
new-lang-info)))
(define lexer-changed? (not (equal? (lang-info-lexer lang-info)
(lang-info-lexer new-lang-info))))
(set! lang-info new-lang-info)
(when (or any-changed? (= gen 1))
(on-changed-lang-info gen new-lang-info))
lexer-changed?)
(define/private (update-tokens-and-parens edit-pos diff)
(define raw-lexer (if other-lang-source
(lang-info-lexer lang-info)
(waive-option module-lexer*)))
;; Determine the position from which we need to start
;; re-tokenizing (this will be less than the edit position) and
;; the initial lexer mode.
(define-values (initial-pos initial-mode effective-lexer)
(cond
[(procedure-arity-includes? raw-lexer 3)
(with-semaphore tokens-sema
;; Find beginning of the token, if any, corresponding to the
;; edit position.
;;
;; An update at the end can result in token-ref returning #f
;; so make an initial adjustment of edit-pos to give to
;; token-ref.
(send tokens search! edit-pos)
(define pos (send tokens get-root-start-position))
(match (token-ref pos)
[(list beg _end (struct* data ([backup backup])))
;; Initially back up by at least 1 (i.e. to the previous
;; token) or by this token's `backup` amount.
(let loop ([pos (- beg (max 1 backup))])
(match (token-ref pos)
[(list beg _end (struct* data ([backup backup])))
(if (< 0 backup)
(loop (- beg backup))
;; Finally, back up one more to get the initial
;; lexer mode, if any. (Why: The mode stored
;; with a token is state with which to read the
;; _next_ token.)
(match (token-ref (sub1 beg))
[(list _beg _end (struct* data ([mode mode])))
(values beg mode raw-lexer)]
[#f (values beg #f raw-lexer)]))]
[#f (values min-position #f raw-lexer)]))]
[#f (values min-position #f raw-lexer)]))]
[(procedure-arity-includes? raw-lexer 1)
(values min-position
'dummy-mode
(λ (port _pos _mode)
(define-values (lexeme attribs paren beg end)
(raw-lexer port))
(values lexeme attribs paren beg end beg 'dummy-mode)))]
[else
(error 'update-tokens-and-parens "Unknown lexer arity")]))
;; Everything before this is valid; allow other threads to
;; progress thru that position of this generation.
(set-update-progress #:position (sub1 initial-pos))
;; Split the token and paren trees.
(define old-tokens (with-semaphore tokens-sema
(send tokens search! initial-pos)
(define-values (t1 t2) (send tokens split-before))
(set! tokens t1)
t2))
(with-semaphore parens-sema
(send parens split-tree initial-pos))
;; Run the lexer until it produces sufficient unchanged tokens.
;; Update token-tree and paren-tree. Track bounds of visible
;; changes to notify via on-changed-tokens.
(define in (lines:open-input-text content initial-pos))
(define-values (min-changed-pos max-changed-pos)
(let tokenize ([pos initial-pos]
[mode initial-mode]
[previous-same? #f]
[contig-same-count 0]
[min-changed-pos max-position]
[max-changed-pos min-position])
(define pos/port (add1 pos))
(define-values (lexeme attribs paren beg/port end/port backup new-mode/ds)
(effective-lexer in pos/port mode))
(define-values (new-mode may-stop?)
(match new-mode/ds
[(struct* dont-stop ([val v])) (values v #f)]
[v (values v #t)]))
(cond
[(eof-object? lexeme)
(values min-changed-pos max-changed-pos)]
[else
(define new-beg (sub1 beg/port))
(define new-end (sub1 end/port))
(define new-span (- new-end new-beg))
(define new-tok (data attribs backup new-mode))
(with-semaphore tokens-sema (insert-last-spec! tokens new-span new-tok))
(with-semaphore parens-sema (send parens add-token paren new-span))
(set-update-progress #:position (sub1 new-end))
;; Detect whether same as before (just shifted by `diff`)
(send old-tokens search! (- new-beg initial-pos diff))
(define old-beg (send old-tokens get-root-start-position))
(define old-end (send old-tokens get-root-end-position))
(define old-span (- old-end old-beg))
(define old-tok (send old-tokens get-root-data))
(define same? (and (equal? new-span old-span)
(equal? new-tok old-tok)))
(define new-contig-same-count (if (and previous-same? same?)
(add1 contig-same-count)
0))
(cond
[(and may-stop?
;; If enough same tokens in a row, assume
;; tokenization has "converged" with old one and
;; there is no need to continue. Here "3" is a
;; WAG. [IIUC the framework colorer feels "1" is
;; enough and relies on lexer dont-stop.]
(>= new-contig-same-count 3))
(send old-tokens search! old-beg)
(define-values (_ keep) (send old-tokens split-after))
(with-semaphore tokens-sema (insert-last! tokens keep))
(define paren-keep-span (- (last-position) new-end))
(with-semaphore parens-sema (send parens merge-tree paren-keep-span))
(values min-changed-pos max-changed-pos)]
[else
;; For purposes of notifying clients to re-color we
;; use a stricter sense of "same" than we do for
;; deciding whether to continue lexing. Here we care
;; only whether the span and attributes are the same
;; (not whether backup or mode changed; those are N/A
;; for visible coloring changes).
(define same-span/attribs?
(and (equal? new-span old-span)
(equal? (data-attribs new-tok) (data-attribs old-tok))))
(tokenize new-end
new-mode
same?
new-contig-same-count
(if same-span/attribs?
min-changed-pos
(min min-changed-pos new-beg))
(if same-span/attribs?
max-changed-pos
(max max-changed-pos new-end)))])])))
(on-changed-tokens updated-generation
min-changed-pos
max-changed-pos)
(set-update-progress #:position max-position))
;; ------------------------------------------------------------
;;
;; Public methods for Emacs commands.
;; This method is safe to call from various threads.
;;
;; The method signature here is similar to that of Emacs'
;; after-change functions: Something changed starting at POS. The
;; text there used to be OLD-LEN chars long, but is now NEW-STR.
(define/public (update! gen pos old-len new-str)
;;(-> generation/c position/c exact-nonnegative-integer? string? any)
(unless (< updated-generation gen)
(raise-argument-error 'update! "valid generation" 0 gen pos old-len new-str))
(unless (<= min-position pos)
(raise-argument-error 'update! "valid position" 1 gen pos old-len new-str))
(async-channel-put update-chan
(list gen pos old-len new-str)))
;; Can be called on any command thread.
(define/public (classify gen pos)
;; (-> generation/c position/c (or/c #f (list/c position/c position/c (or/c symbol? hash-eq?))
(block-until-updated-thru gen pos)
(match (with-semaphore tokens-sema (token-ref pos))
[(list beg end (struct* data ([attribs attribs])))
(list beg end attribs)]
[#f #f]))
;; Can be called on any command thread.
(define/public (get-tokens gen
[from min-position]
[upto max-position])
(block-until-updated-thru gen upto)
(let loop ([pos from])
(match (with-semaphore tokens-sema (token-ref pos))
[(list beg end (struct* data ([attribs attribs])))
(if (<= end upto)
(cons (list beg end attribs)
(loop end))
null)]
[#f null])))
;; Methods for Emacs navigation and indent commands.
;;
;; These command methods work by calling various drracket:xyz
;; functions, supplying `this` as the color-textoid<%> argument.
;; In other words, those functions will "call back" use the
;; textoid methods.
;;
;; These command methods call block-until-updated-thru, to wait
;; until the updater thread has progressed far enough to support
;; the command.
;;
;; These command methods take the tokens and parens semaphores for
;; the dynamic extent the call to the drracket:xyz function. As a result
;; the textoid methods need not. This is signficantly faster (e.g. 2X).
;;
;; Can be called on any command thread.
(define/public (grouping gen pos dir limit count)
(cond
[(<= count 0) pos]
[else
(block-until-updated-thru gen
(case dir
[(up backward) min-position]
[(down forward) max-position]))
(define grouping-position (lang-info-grouping-position lang-info))
(let loop ([pos pos]
[count count])
(match (with-semaphore tokens-sema
(with-semaphore parens-sema
(match (grouping-position this pos limit dir)
;; Handle case where it returns #t, meaning
;; "use default s-expr grouping". That spec
;; slightly predates the addition of
;; syntax-color/racket-navigation --- the
;; availability of which probably means that
;; this #t value should no longer be returned?
;; In other words, if a lang wants s-expr nav,
;; its lang info should either not supply any
;; drracket:grouping-position at all, or,
;; supply racket-grouping-position as that?
[#t
(when (equal? grouping-position racket-grouping-position)
(error 'grouping "racket-grouping-position returned #t"))
(racket-grouping-position this pos limit dir)]
[v v])))
[#f #f]
[(? number? new-pos)
(cond [(< 1 count) (loop new-pos (sub1 count))]
[(= new-pos pos) #f]
[else new-pos])]))]))
;; Can be called on any command thread.
(define/public (indent-line-amount gen pos)
(block-until-updated-thru gen pos)
(with-semaphore tokens-sema
(with-semaphore parens-sema
(or ((lang-info-line-indenter lang-info) this pos) ;may return #f meaning...
(racket-amount-to-indent this pos)))))
;; Can be called on any command thread.
(define/public (indent-range-amounts gen from upto)
(define range-indenter (lang-info-range-indenter lang-info))
(cond [(not range-indenter) #f]
[else
(block-until-updated-thru gen upto)
(with-semaphore tokens-sema
(with-semaphore parens-sema
(range-indenter this from upto)))]))
;; Can be called on any command thread.
(define/public (submit-predicate in eos?)
(match (lang-info-submit-predicate lang-info)
[(? procedure? p) (p in eos?)]
[_ #f]))
;; -----------------------------------------------------------------
;; color-textoid<%> methods.
;;
;; Warning: As discussed above, these are thread-safe to call only
;; from the dyanamic extent of the `grouping`,
;; `indent-line-amount`, or `indent-range-amounts` methods.
(define/public (last-position)
(lines:text-length content))
(define/public (get-character pos)
(if (< pos (lines:text-length content))
(string-ref (lines:get-text content pos (add1 pos)) 0)
#\nul))
(define/public (get-text from upto)
(lines:get-text content from (if (eq? upto 'eof) (last-position) upto)))
(define/public (position-paragraph pos [eol? #f])
(lines:position->line content (min pos (last-position))))
(define/public (paragraph-start-position para)
(lines:line->start content (max 0 (min para (lines:text-line-count content)))))
(define/public (paragraph-end-position para)
(cond [(<= (lines:text-line-count content) (add1 para))
(lines:text-length content)]
[else
(sub1 (lines:line->start content (add1 para)))]))
(define/public (classify-position* position)
(send tokens search! position)
(match (send tokens get-root-data)
[(struct* data ([attribs (app attribs->table table)])) table]
[#f #f]))
(define/public (classify-position position)
(send tokens search! position)
(match (send tokens get-root-data)
[(struct* data ([attribs (app attribs->type type)])) type]
[#f #f]))
(define/public (get-token-range position)
(send tokens search! position)
(values (send tokens get-root-start-position)
(send tokens get-root-end-position)))
(define/public (get-backward-navigation-limit pos)
0)
(define/public (backward-match position cutoff)
(let ([x (internal-backward-match position cutoff)])
(cond
[(or (eq? x 'open) (eq? x 'beginning)) #f]
[else x])))
(define/private (internal-backward-match position cutoff)
(let ([position (skip-whitespace position 'backward #t)])
(define-values (start end error) (send parens match-backward position))
(cond
[(and start end (not error))
(let ((match-pos start))
(cond
((>= match-pos cutoff) match-pos)
(else #f)))]
[(and start end error) #f]
[else
(send tokens search! (sub1 position))
(define tok-start (send tokens get-root-start-position))
(cond
[(send parens is-open-pos? tok-start) 'open]
[(= tok-start position) 'beginning]
[else tok-start])])))
(define/public (backward-containing-sexp position cutoff)
(let loop ([cur-pos position])
(let ([p (internal-backward-match cur-pos cutoff)])
(cond
[(eq? 'open p)
;; [Comment from color.rkt: "Should this function skip
;; backwards past whitespace? the docs seem to indicate
;; it does, but it doesn't really."]
cur-pos]
[(eq? 'beginning p) #f]
[(not p) #f]
(else (loop p))))))
(define/public (forward-match position cutoff)
(do-forward-match position cutoff #t))
(define/private (do-forward-match position cutoff skip-whitespace?)
(let ([position (if skip-whitespace?
(skip-whitespace position 'forward #t)
position)])
(define-values (start end error) (send parens match-forward position))
(cond
[(and start end (not error))
(cond
[(<= end cutoff) end]
[else #f])]
[(and start end error) #f]
[else
(skip-past-token position)])))
(define/private (skip-past-token position)
(send tokens search! position)
(define start (send tokens get-root-start-position))
(define end (send tokens get-root-end-position))
(cond
[(or (send parens is-close-pos? start)
(= end position))
#f]
[else end]))
(define/public (skip-whitespace position direction comments?)
(cond
[(and (eq? direction 'forward) (>= position (last-position))) position]
[(and (eq? direction 'backward) (<= position 0)) position]
[else
(send tokens search! (if (eq? direction 'backward)
(sub1 position)
position))
(match (send tokens get-root-data)
[(struct* data ([attribs (app attribs->type type)]))
(cond
[(or (eq? 'white-space type)
(and comments? (eq? 'comment type)))
(skip-whitespace (if (eq? direction 'forward)
(send tokens get-root-end-position)
(send tokens get-root-start-position))
direction
comments?)]
[else position])]
[#f position])]))
(define/public (get-regions)
'((0 end)))))
(define default-lexer racket-lexer)
(define default-module-language #f)
(define default-paren-matches '((\( \)) (\[ \]) (\{ \})))
(define default-quote-matches '(#\" #\|))
(define default-lang-info
(lang-info default-module-language
default-lexer
default-paren-matches
default-quote-matches
racket-grouping-position
racket-amount-to-indent
#f
#f
#f))
(define (read-lang-info* in)
(define info (or (with-handlers ([values (λ _ #f)])
(read-language in (λ _ #f)))
(λ (_key default) default)))
(define-values (_line _col end-pos) (port-next-location in))
(define mod-lang (safe-info-module-language info))
(values (lang-info mod-lang
(info 'color-lexer default-lexer)
(info 'drracket:paren-matches default-paren-matches)
(info 'drracket:quote-matches default-quote-matches)
(info 'drracket:grouping-position racket-grouping-position)
(info 'drracket:indentation racket-amount-to-indent)
(info 'drracket:range-indentation #f)
(info 'drracket:submit-predicate #f)
(comment-delimiters info mod-lang))
end-pos))
;; Handle the module-language lang info key, as documented at
;; <https://docs.racket-lang.org/syntax/reader-helpers.html#%28mod-path._syntax%2Fmodule-reader%29>.
;; (info-proc -> (or/c #f string?)
(define (safe-info-module-language info)
(define (handle v)
(match v
[(== default-module-language) default-module-language]
[(? module-path? mp)
(~a mp)]
[(? syntax? stx)
#:when (module-path? (syntax->datum stx))
(~a (syntax->datum stx))]
[(? procedure? p)
(handle v)]
[hopeless
(log-racket-mode-debug "Ignoring value returned for module-language key: ~v"
info hopeless)
default-module-language]))
(handle (info 'module-language default-module-language)))
;; Return (list start continue end padding)
(define (comment-delimiters info mod-lang)
(define (fallback)
;; Fallback when langs don't support the info key, or the value
;; isn't as expected.
(define (root mp-str) ;e.g. 'racket and 'racket/base => 'racket
(match mp-str
[(pregexp "^([^/]+)" (list _ str))
(string->symbol str)]
[_ #f]))
(match (root mod-lang)
["scribble" '("@;" "@;" "" " ")]
["rhombus" '("//" "//" "" " ")]
[_ '(";;" ";;" "" " ")]))
(match (info 'drracket:comment-delimiters #f)
[#f (fallback)]
[(list* (list 'line (? string? start) (? string? padding))
_other-styles)
(list start start "" padding)]
[(list* (list 'region (? string? start) (? string? continue) (? string? end) (? string? padding))
_other-styles)
(list start continue end padding)]
[unexpected
(log-racket-mode-warning
"drracket:comment-delimiters from mod-lang ~v\n unexpected value: ~v"
mod-lang
unexpected)
(fallback)]))
(define (read-lang-info in)
(define-values (v _pos) (read-lang-info* in))
v)
(define (attribs->type attribs)
(match attribs
[(? symbol? s) s]
[(? hash? ht) (hash-ref ht 'type 'unknown)]
[_ 'unknown]))
(define (attribs->table attribs)
(if (symbol? attribs)
(hasheq 'type attribs)
attribs))
;; This could be moved to its own file.
(module monitor racket/base
(require racket/match
syntax/parse/define)
(provide make-monitor
monitor?
progress
wait
wait-evt)
(struct monitor ([waiters #:mutable] sema) #:authentic)
(struct waiter (pred sema) #:transparent #:authentic)
(define (make-monitor)
(monitor null (make-semaphore 1)))
(define-simple-macro (with-semaphore sema e:expr ...+)
(call-with-semaphore sema (λ () e ...)))
;; To be called by a worker thread, to make progress that might cause
;; some waiter's predicate to become true. The thunk is called within
;; the monitor's semaphore, so it is safe for it to e.g. set! multiple
;; variables.
(define (progress m thunk)
(with-semaphore (monitor-sema m)
(thunk)
(set-monitor-waiters!
m
(let loop ([waiters (monitor-waiters m)])
(match waiters
[(list) (list)]
[(cons w more)
(cond [((waiter-pred w))
(semaphore-post (waiter-sema w))
(loop more)] ;remove
[else ;keep
(cons w (loop more))])])))))
;; To be called by any number of observer threads, to wait until a
;; predicate becomes true. The predicate is checked initially in case
;; it is already true, but thereafter only whenever a worker thread
;; calls `progress`. The predicate is called within the monitor's
;; semaphore (if the `progress` thunk set!s multiple vars, it's safe
;; for the pred to check them).
(define (wait m pred)
(unless (call-with-semaphore (monitor-sema m) pred) ;fast path
(semaphore-wait (wait-evt m pred))))
;; Like `wait` but returns a synchronizable event.
(define (wait-evt m pred)
(cond [(call-with-semaphore (monitor-sema m) pred) ;fast path
always-evt]
[else
(define pred-sema (make-semaphore 0))
(with-semaphore (monitor-sema m)
(set-monitor-waiters! m (cons (waiter pred pred-sema)
(monitor-waiters m))))
pred-sema]))
(module+ example
;; Some variables that a worker thread will increase monotonically.
(define i 0)
(define j 0)
;; A monitor object
(define m (make-monitor))
;; Some threads that want to wait for certain values.
(void (thread (λ ()
(define (pred-0) (and (<= 0 i)))
(wait m pred-0)
(displayln "pred-0 became true (fast path)"))))
(void (thread (λ ()
(define (pred-i-3-j-6) (and (<= 3 i) (<= 6 j)))
(wait m pred-i-3-j-6)
(displayln "pred-i-3-j-6 became true"))))
(void (thread (λ ()
(define (pred-i-5) (<= 5 i))
(wait m pred-i-5)
(displayln "pred-i-5 became true"))))
;; A worker thread.
(let loop ()
(progress m (λ ()
(set! i (add1 i))
(set! j (add1 j))
(displayln (list i j))))
(when (< i 10)
(sleep 0.5)
(loop)))))
(require 'monitor)
|