From 5548734ef75747a11da2f274422bcc520139d6bc Mon Sep 17 00:00:00 2001 From: Hugo Thunnissen Date: Sun, 16 Jul 2023 21:48:05 +0200 Subject: [PATCH] Implement parser interruption on user input --- phpinspect-buffer.el | 11 +++ phpinspect-parser.el | 113 ++++++++++++++++--------- phpinspect.el | 197 +++++++++++++++++++++---------------------- 3 files changed, 183 insertions(+), 138 deletions(-) diff --git a/phpinspect-buffer.el b/phpinspect-buffer.el index 56df0c2..f16cecd 100644 --- a/phpinspect-buffer.el +++ b/phpinspect-buffer.el @@ -55,6 +55,7 @@ linked with." (let* ((map (phpinspect-make-bmap)) (buffer-map (phpinspect-buffer-map buffer)) (ctx (phpinspect-make-pctx + :interrupt-predicate #'input-pending-p :bmap map :incremental t :previous-bmap buffer-map @@ -81,6 +82,16 @@ linked with." (cl-defmethod phpinspect-buffer-register-edit ((buffer phpinspect-buffer) (start integer) (end integer) (pre-change-length integer)) + "Mark a region of the buffer as edited." + + ;; Take into account "atoms" (tokens without clear delimiters like words, + ;; variables and object attributes. The meaning of these tokens will change as + ;; they grow or shrink, so their ful regions need to be marked for a reparse). + (save-excursion + (goto-char start) + (when (looking-back "\\($->|::\\)?[^][)(}{[:blank:]\n;'\"]+" nil t) + (setq start (- start (length (match-string 0)))))) + (phpinspect-edtrack-register-edit (phpinspect-buffer-edit-tracker buffer) start end pre-change-length)) diff --git a/phpinspect-parser.el b/phpinspect-parser.el index 9d9e557..e5fa194 100644 --- a/phpinspect-parser.el +++ b/phpinspect-parser.el @@ -37,6 +37,69 @@ (define-inline phpinspect--word-end-regex () (inline-quote "\\([[:blank:]]\\|[^0-9a-zA-Z_]\\)"))) +(defvar phpinspect-parse-context nil + "An instance of `phpinspect-pctx' that is used when +parsing. Usually used in combination with +`phpinspect-with-parse-context'") + +(defmacro phpinspect-with-parse-context (ctx &rest body) + (declare (indent 1)) + (let ((old-ctx phpinspect-parse-context)) + `(unwind-protect + (progn + (setq phpinspect-parse-context ,ctx) + ,@body) + (setq phpinspect-parse-context ,old-ctx)))) + +(cl-defstruct (phpinspect-pctx (:constructor phpinspect-make-pctx)) + "Parser Context" + (incremental nil) + (interrupt-threshold (time-convert '(2 . 1000)) + :documentation + "After how much time `interrupt-predicate' +should be polled. This is 2ms by default.") + (-start-time nil + :documentation "The time at which the parse started. +This variable is for private use and not always set.") + (interrupt-predicate nil + :documentation + "A function that is called in intervals during parsing when +set. If this function returns a non-nil value, the parse process +is interrupted and the symbol `phpinspect-parse-interrupted' is +thrown.") + (edtrack nil + :type phpinspect-edtrack) + (bmap (phpinspect-make-bmap) + :type phpinspect-bmap) + (previous-bmap nil + :type phpinspect-bmap) + (whitespace-before "" + :type string)) + +(defsubst phpinspect-pctx-check-interrupt (pctx) + (unless (phpinspect-pctx--start-time pctx) + (setf (phpinspect-pctx--start-time pctx) (time-convert nil))) + + ;; Interrupt when blocking too long while input is pending. + (when (and (time-less-p (phpinspect-pctx-interrupt-threshold pctx) + (time-since (phpinspect-pctx--start-time pctx))) + (funcall (phpinspect-pctx-interrupt-predicate pctx))) + (throw 'phpinspect-parse-interrupted nil))) + + + +(defsubst phpinspect-pctx-register-token (pctx token start end) + (phpinspect-bmap-register + (phpinspect-pctx-bmap pctx) start end token (phpinspect-pctx-consume-whitespace pctx))) + +(defsubst phpinspect-pctx-register-whitespace (pctx whitespace) + (setf (phpinspect-pctx-whitespace-before pctx) whitespace)) + +(defsubst phpinspect-pctx-consume-whitespace (pctx) + (let ((whitespace (phpinspect-pctx-whitespace-before pctx))) + (setf (phpinspect-pctx-whitespace-before pctx) "") + whitespace)) + (defun phpinspect-list-handlers () (let ((handlers)) (mapatoms (lambda (handler) @@ -370,9 +433,14 @@ parser function is then returned in byte-compiled form." (if (and phpinspect-parse-context (phpinspect-pctx-incremental phpinspect-parse-context)) + (let ((func (phpinspect-parser-compile-incremental (symbol-value parser-symbol)))) - (lambda (&rest arguments) - (apply func phpinspect-parse-context arguments))) + (if (phpinspect-pctx-interrupt-predicate phpinspect-parse-context) + (lambda (&rest arguments) + (phpinspect-pctx-check-interrupt phpinspect-parse-context) + (apply func phpinspect-parse-context arguments)) + (lambda (&rest arguments) + (apply func phpinspect-parse-context arguments)))) (or (symbol-function parser-symbol) (defalias parser-symbol (phpinspect-parser-compile (symbol-value parser-symbol))))))) @@ -457,43 +525,6 @@ token is \";\", which marks the end of a statement in PHP." ;; Return tokens))))) -(defvar phpinspect-parse-context nil - "An instance of `phpinspect-pctx' that is used when -parsing. Usually used in combination with -`phpinspect-with-parse-context'") - -(defmacro phpinspect-with-parse-context (ctx &rest body) - (declare (indent 1)) - (let ((old-ctx phpinspect-parse-context)) - `(unwind-protect - (progn - (setq phpinspect-parse-context ,ctx) - ,@body) - (setq phpinspect-parse-context ,old-ctx)))) - -(cl-defstruct (phpinspect-pctx (:constructor phpinspect-make-pctx)) - "Parser Context" - (incremental nil) - (edtrack nil - :type phpinspect-edtrack) - (bmap (phpinspect-make-bmap) - :type phpinspect-bmap) - (previous-bmap nil - :type phpinspect-bmap) - (whitespace-before "" - :type string)) - -(defsubst phpinspect-pctx-register-token (pctx token start end) - (phpinspect-bmap-register - (phpinspect-pctx-bmap pctx) start end token (phpinspect-pctx-consume-whitespace pctx))) - -(defsubst phpinspect-pctx-register-whitespace (pctx whitespace) - (setf (phpinspect-pctx-whitespace-before pctx) whitespace)) - -(defsubst phpinspect-pctx-consume-whitespace (pctx) - (let ((whitespace (phpinspect-pctx-whitespace-before pctx))) - (setf (phpinspect-pctx-whitespace-before pctx) "") - whitespace)) (defun phpinspect-make-incremental-parser-function (tree-type handler-list &optional delimiter-predicate) "Like `phpinspect-make-parser-function', but returned function is able to reuse an already parsed tree." @@ -516,6 +547,7 @@ parsing. Usually used in combination with (previous-bmap (phpinspect-pctx-previous-bmap context)) (edtrack (phpinspect-pctx-edtrack context)) (taint-iterator (when edtrack (phpinspect-edtrack-make-taint-iterator edtrack))) + (check-interrupt (phpinspect-pctx-interrupt-predicate context)) ;; Loop variables (start-position) @@ -551,6 +583,9 @@ parsing. Usually used in combination with (goto-char current-end-position) + (when check-interrupt + (phpinspect-pctx-check-interrupt context)) + ;; Skip over whitespace after so that we don't do a full ;; run down all of the handlers during the next iteration (when (looking-at (phpinspect-handler-regexp 'whitespace)) diff --git a/phpinspect.el b/phpinspect.el index 0816ca3..481796d 100644 --- a/phpinspect.el +++ b/phpinspect.el @@ -282,87 +282,88 @@ TODO: - Respect `eldoc-echo-area-use-multiline-p` - This function is too big and has repetitive code. Split up and simplify. " - (let* ((token-map (phpinspect-buffer-parse-map phpinspect-current-buffer)) - (resolvecontext (phpinspect-get-resolvecontext token-map (point))) - (parent-token (car (phpinspect--resolvecontext-enclosing-tokens - resolvecontext))) - (enclosing-token (cadr (phpinspect--resolvecontext-enclosing-tokens - resolvecontext))) - (statement (phpinspect--resolvecontext-subject resolvecontext)) - (arg-list) - (type-resolver (phpinspect--make-type-resolver-for-resolvecontext - resolvecontext)) - (static)) - - (phpinspect--log "Eldoc statement before checking outside list: %s" statement) - (when (and (phpinspect-list-p parent-token) enclosing-token) - (setq statement - (phpinspect-find-statement-before-point - token-map (phpinspect-bmap-token-meta token-map enclosing-token) - (phpinspect-meta-end - (phpinspect-bmap-token-meta token-map parent-token))))) - - (phpinspect--log "Enclosing token: %s" enclosing-token) - (phpinspect--log "Eldoc statement: %s" statement) - - (setq arg-list (seq-find #'phpinspect-list-p (reverse statement))) - - (when (and (phpinspect-list-p arg-list) - enclosing-token - (or (phpinspect-object-attrib-p (car (last statement 2))) - (setq static (phpinspect-static-attrib-p (car (last statement 2)))))) - - ;; Set resolvecontext subject to the last statement in the enclosing token, minus - ;; the method name. The last enclosing token is an incomplete list, so point is - ;; likely to be at a location inside a method call like "$a->b->doSomething(". The - ;; resulting subject would be "$a->b". - (setf (phpinspect--resolvecontext-subject resolvecontext) - (phpinspect--get-last-statement-in-token (butlast statement 2))) - - (let* ((type-of-previous-statement - (phpinspect-resolve-type-from-context resolvecontext type-resolver)) - (method-name-sym (phpinspect-intern-name (cadr (cadar (last statement 2))))) - (class (phpinspect-project-get-class-create - (phpinspect--cache-get-project-create - (phpinspect--get-or-create-global-cache) - (phpinspect--resolvecontext-project-root resolvecontext)) - type-of-previous-statement)) - (method (when class - (if static - (phpinspect--class-get-static-method class method-name-sym) + (catch 'phpinspect-parse-interrupted + (let* ((token-map (phpinspect-buffer-parse-map phpinspect-current-buffer)) + (resolvecontext (phpinspect-get-resolvecontext token-map (point))) + (parent-token (car (phpinspect--resolvecontext-enclosing-tokens + resolvecontext))) + (enclosing-token (cadr (phpinspect--resolvecontext-enclosing-tokens + resolvecontext))) + (statement (phpinspect--resolvecontext-subject resolvecontext)) + (arg-list) + (type-resolver (phpinspect--make-type-resolver-for-resolvecontext + resolvecontext)) + (static)) + + (phpinspect--log "Eldoc statement before checking outside list: %s" statement) + (when (and (phpinspect-list-p parent-token) enclosing-token) + (setq statement + (phpinspect-find-statement-before-point + token-map (phpinspect-bmap-token-meta token-map enclosing-token) + (phpinspect-meta-end + (phpinspect-bmap-token-meta token-map parent-token))))) + + (phpinspect--log "Enclosing token: %s" enclosing-token) + (phpinspect--log "Eldoc statement: %s" statement) + + (setq arg-list (seq-find #'phpinspect-list-p (reverse statement))) + + (when (and (phpinspect-list-p arg-list) + enclosing-token + (or (phpinspect-object-attrib-p (car (last statement 2))) + (setq static (phpinspect-static-attrib-p (car (last statement 2)))))) + + ;; Set resolvecontext subject to the last statement in the enclosing token, minus + ;; the method name. The last enclosing token is an incomplete list, so point is + ;; likely to be at a location inside a method call like "$a->b->doSomething(". The + ;; resulting subject would be "$a->b". + (setf (phpinspect--resolvecontext-subject resolvecontext) + (phpinspect--get-last-statement-in-token (butlast statement 2))) + + (let* ((type-of-previous-statement + (phpinspect-resolve-type-from-context resolvecontext type-resolver)) + (method-name-sym (phpinspect-intern-name (cadr (cadar (last statement 2))))) + (class (phpinspect-project-get-class-create + (phpinspect--cache-get-project-create + (phpinspect--get-or-create-global-cache) + (phpinspect--resolvecontext-project-root resolvecontext)) + type-of-previous-statement)) + (method (when class + (if static + (phpinspect--class-get-static-method class method-name-sym) (phpinspect--class-get-method class method-name-sym))))) - (phpinspect--log "Eldoc method name: %s" method-name-sym) - (phpinspect--log "Eldoc type of previous statement: %s" - type-of-previous-statement) - (phpinspect--log "Eldoc method: %s" method) - (when method - (let ((arg-count -1) - (comma-count - (length (seq-filter #'phpinspect-comma-p arg-list)))) - (concat (truncate-string-to-width - (phpinspect--function-name method) phpinspect-eldoc-word-width) ": (" - (mapconcat - (lambda (arg) - (setq arg-count (+ arg-count 1)) - (if (= arg-count comma-count) - (propertize (concat - "$" - (truncate-string-to-width - (car arg) - phpinspect-eldoc-word-width) - " " - (phpinspect--format-type-name (or (cadr arg) ""))) - 'face 'eldoc-highlight-function-argument) - (concat "$" - (truncate-string-to-width (car arg) - phpinspect-eldoc-word-width) - (if (cadr arg) " " "") - (phpinspect--format-type-name (or (cadr arg) ""))))) - (phpinspect--function-arguments method) - ", ") - "): " - (phpinspect--format-type-name - (phpinspect--function-return-type method))))))))) + (phpinspect--log "Eldoc method name: %s" method-name-sym) + (phpinspect--log "Eldoc type of previous statement: %s" + type-of-previous-statement) + (phpinspect--log "Eldoc method: %s" method) + (when method + (let ((arg-count -1) + (comma-count + (length (seq-filter #'phpinspect-comma-p arg-list)))) + (concat (truncate-string-to-width + (phpinspect--function-name method) phpinspect-eldoc-word-width) ": (" + (mapconcat + (lambda (arg) + (setq arg-count (+ arg-count 1)) + (if (= arg-count comma-count) + (propertize (concat + "$" + (truncate-string-to-width + (car arg) + phpinspect-eldoc-word-width) + " " + (phpinspect--format-type-name (or (cadr arg) ""))) + 'face 'eldoc-highlight-function-argument) + (concat "$" + (truncate-string-to-width (car arg) + phpinspect-eldoc-word-width) + (if (cadr arg) " " "") + (phpinspect--format-type-name (or (cadr arg) ""))))) + (phpinspect--function-arguments method) + ", ") + "): " + (phpinspect--format-type-name + (phpinspect--function-return-type method)))))))))) (cl-defstruct (phpinspect--assignment (:constructor phpinspect--make-assignment)) @@ -974,9 +975,6 @@ level of a token. Nested variables are ignored." (resolvecontext &optional static) "Suggest object or class attributes at point. -TOKEN-TREE must be a syntax tree containing enough context to -infer the types of the preceding statements - RESOLVECONTEXT must be a structure of the type `phpinspect--resolvecontext'. The PHP type of its subject is resolved to provide completion candidates. @@ -1097,22 +1095,23 @@ static variables and static methods." arg))) (insert "("))) ((eq command 'candidates) - (let ((completion-list (phpinspect--make-completion-list)) - (candidates)) - (dolist (completion (phpinspect--suggest-at-point)) - (phpinspect--completion-list-add - completion-list - (phpinspect--make-completion completion))) - - (setq candidates - (seq-filter (lambda (completion) - (when completion - (string-match (concat "^" (regexp-quote arg)) - completion))) - (phpinspect--completion-list-strings - completion-list))) - (setq phpinspect--last-completion-list completion-list) - candidates)) + (catch 'phpinspect-parse-interrupted + (let ((completion-list (phpinspect--make-completion-list)) + (candidates)) + (dolist (completion (phpinspect--suggest-at-point)) + (phpinspect--completion-list-add + completion-list + (phpinspect--make-completion completion))) + + (setq candidates + (seq-filter (lambda (completion) + (when completion + (string-match (concat "^" (regexp-quote arg)) + completion))) + (phpinspect--completion-list-strings + completion-list))) + (setq phpinspect--last-completion-list completion-list) + candidates))) ((eq command 'annotation) (concat " " (phpinspect--completion-annotation (phpinspect--completion-list-get-metadata