+++ /dev/null
-;;; graphql-mode.el --- Major mode for editing GraphQL schemas -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2016, 2017 David Vazquez Pua
-
-;; Author: David Vazquez Pua <davazpNOSPAMgmail.com>
-;; Keywords: languages
-;; Package-Requires: ((emacs "25.1"))
-;; Homepage: https://github.com/davazp/graphql-mode
-;; Package-Version: 20240218.1603
-;; Package-Revision: 2183895ea793
-
-;; This program is free software; you can redistribute it and/or modify
-;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-
-;; This program is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-;; GNU General Public License for more details.
-
-;; You should have received a copy of the GNU General Public License
-;; along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-;;; Commentary:
-
-;; This package implements a major mode to edit GraphQL schemas and
-;; query. The basic functionality includes:
-;;
-;; - Syntax highlight
-;; - Automatic indentation
-;;
-;; Additionally, it is able to
-;; - Sending GraphQL queries to an end-point URL
-;;
-;; Files with the .graphql and .gql extensions are automatically
-;; opened with this mode.
-
-
-;;; Code:
-
-(require 'newcomment)
-(require 'json)
-(require 'url)
-(require 'cl-lib)
-(require 'let-alist)
-
-;;; User Customizations:
-
-(defgroup graphql nil
- "Major mode for editing GraphQL schemas and queries."
- :tag "GraphQL"
- :group 'languages)
-
-(defcustom graphql-indent-level 2
- "Number of spaces for each indentation step in `graphql-mode'."
- :tag "GraphQL"
- :type 'integer
- :safe 'integerp
- :group 'graphql)
-
-(defcustom graphql-url nil
- "URL address of the graphql server endpoint."
- :tag "GraphQL"
- :type 'string
- :group 'graphql)
-
-(defcustom graphql-variables-file nil
- "File name containing graphql variables."
- :tag "GraphQL"
- :type 'file
- :group 'graphql)
-
-(defcustom graphql-extra-headers '()
- "Headers to send to the graphql endpoint."
- :tag "GraphQL"
- :type '(repeat sexp)
- :group 'graphql)
-
-(defun graphql-locate-config (dir)
- "Locate a graphql config starting in DIR."
- (if-let ((config-dir (locate-dominating-file dir ".graphqlconfig")))
- (concat config-dir ".graphqlconfig")
- (error "Could not find a .graphqlconfig file")))
-
-(defun graphql--completing-read-endpoint (endpoints)
- "Select an endpoint configuration from a list of ENDPOINTS."
- (completing-read "Select Graphql Endpoint:" (mapcar 'car endpoints)))
-
-(defun graphql-open-config ()
- "Open the graphql config."
- (interactive)
- (find-file (graphql-locate-config ".")))
-
-(defun graphql-select-endpoint ()
- "Set parameters based off of the endpoints listed in a .graphqlconfig file."
- (interactive)
- (let ((config (json-read-file (graphql-locate-config "."))))
- (let-alist config
- (if-let ((endpoints .extensions.endpoints)
- (endpoint (cdr (assq (intern (graphql--completing-read-endpoint endpoints)) endpoints))))
- (let-alist endpoint
- (setq graphql-url .url
- graphql-extra-headers .headers))
- (error "No endpoint configurations in .graphqlconfig")))))
-
-(defun graphql-encode-json (query &optional operation variables)
- "Put together a json like object with QUERY, OPERATION, and VARIABLES."
- (let* ((body '()))
- (push (cons 'query query) body)
- (when (and operation (not (string= operation "")))
- (push (cons 'operationName operation) body))
- (when variables
- (push (cons 'variables variables) body))
- (json-encode body)))
-
-(defun graphql--query (query &optional operation variables)
- "Send QUERY to the server and return the response.
-
-The query is sent as a HTTP POST request to the URL at
-`graphql-url'. The query can be any GraphQL definition (query,
-mutation or subscription). OPERATION is a name for the
-operation. VARIABLES is the JSON string that specifies the values
-of the variables used in the query."
- ;; Note that we need to get the value of graphql-url in the current
- ;; before before we switch to the temporary one.
- (let ((url graphql-url))
- (graphql-post-request url query operation variables)))
-
-(declare-function request "request")
-(declare-function request-response-data "request")
-(declare-function request-response--raw-header "request")
-
-(defun graphql-post-request (url query &optional operation variables)
- "Make post request to graphql server with url and body.
-
-URL hostname, path, search parameters, such as operationName and variables
-QUERY query definition(s) of query, mutation, and/or subscription
-OPERATION name of the operation if multiple definition is given in QUERY
-VARIABLES list of variables for query operation"
- (or (require 'request nil t)
- (error "graphql-post-request needs the request package. \
-Please install it and try again."))
- (let* ((body (graphql-encode-json query operation variables))
- (headers (append '(("Content-Type" . "application/json")) graphql-extra-headers)))
- (request url
- :type "POST"
- :data body
- :headers headers
- :parser 'json-read
- :sync t
- :complete (lambda (&rest _)
- (message "%s" (if (string-equal "" operation)
- url
- (format "%s?operationName=%s"
- url operation)))))))
-
-(defun graphql-beginning-of-query ()
- "Move the point to the beginning of the current query."
- (interactive)
- (goto-char (or (syntax-ppss-toplevel-pos (syntax-ppss (line-beginning-position)))
- (line-beginning-position)))
- (back-to-indentation))
-
-(defun graphql-end-of-query ()
- "Move the point to the end of the current query."
- (interactive)
- (while (and (< (point) (point-max))
- (or (> (current-indentation) 0)
- (> (car (syntax-ppss)) 0)))
- (forward-line 1)))
-
-(defun graphql-current-query ()
- "Return the current query/mutation/subscription definition."
- (let ((start
- (save-excursion
- (graphql-beginning-of-query)
- (point)))
- (end
- (save-excursion
- (graphql-end-of-query)
- (point))))
- (if (not (equal start end))
- (buffer-substring-no-properties start end)
- (save-excursion
- (let ((saved-point (point))
- (line (thing-at-point 'line t)))
- (when (string-match-p (regexp-quote "}") line)
- (search-backward "}" (beginning-of-line)))
- (when (string-match-p (regexp-quote "{") line)
- (search-forward "{" (end-of-line)))
- (if (= (point) saved-point)
- nil
- (graphql-current-query)))))))
-
-(defun graphql-current-operation ()
- "Return the name of the current graphql query."
- (let* ((query
- (save-excursion
- (replace-regexp-in-string "^[ \t\n]*" ""
- (or (graphql-current-query) ""))))
- (tokens
- (split-string query "[ \f\t\n\r\v]+"))
- (first (nth 0 tokens)))
-
- (if (or (string-equal first "{") (string-equal first ""))
- nil
- (replace-regexp-in-string "[({].*" "" (nth 1 tokens)))))
-
-(defun graphql-current-variables (filename)
- "Return the current variables contained in FILENAME."
- (if (and filename
- (not (string-equal filename ""))
- (not (file-directory-p filename))
- (file-exists-p filename))
- (condition-case nil
- (progn (get-buffer-create (find-file-noselect filename))
- (json-read-file filename))
- (error nil))
- nil))
-
-(define-minor-mode graphql-query-response-mode
- "Allows GraphQL query response buffer to be closed with (q)"
- :lighter " GraphQL Response"
- :keymap (let ((map (make-sparse-keymap)))
- (define-key map (kbd "q") 'quit-window)
- map))
-
-(defun graphql-send-query (&optional prompt)
- "Send the current GraphQL query/mutation/subscription to server.
-With \\[universal-argument] PROMPT, prompt for
-`graphql-url'/`graphql-variables-file'."
- (interactive "P")
- (let* ((url (or (and (not prompt) graphql-url)
- (read-string "GraphQL URL: " graphql-url)))
- (var (or (and (not prompt) graphql-variables-file)
- (read-file-name "GraphQL Variables: " nil graphql-variables-file))))
- (let ((graphql-url url)
- (graphql-variables-file var))
-
- (let* ((query (buffer-substring-no-properties (point-min) (point-max)))
- (operation (graphql-current-operation))
- (variables (graphql-current-variables var))
- (response (graphql--query query operation variables)))
- (with-current-buffer-window
- "*GraphQL*" 'display-buffer-pop-up-window nil
- (erase-buffer)
- (when (fboundp 'json-mode)
- (json-mode))
- (insert (json-encode (request-response-data response)))
- (json-pretty-print-buffer)
- (goto-char (point-max))
- (insert "\n\n"
- (propertize (request-response--raw-header response)
- 'face 'font-lock-comment-face
- 'font-lock-face 'font-lock-comment-face))
- (graphql-query-response-mode))))
- ;; If the query was successful, then save the value of graphql-url
- ;; in the current buffer (instead of the introduced local
- ;; binding).
- (setq graphql-url url)
- (setq graphql-variables-file var)
- nil))
-
-(defvar graphql-mode-map
- (let ((map (make-sparse-keymap)))
- (define-key map (kbd "C-c C-c") 'graphql-send-query)
- (define-key map (kbd "C-c C-l") 'graphql-select-endpoint)
- (define-key map (kbd "C-c e h") 'graphql-edit-headers)
- map)
- "Key binding for GraphQL mode.")
-
-(defvar graphql-mode-syntax-table
- (let ((st (make-syntax-table)))
- (modify-syntax-entry ?\# "<" st)
- (modify-syntax-entry ?\n ">" st)
- (modify-syntax-entry ?\$ "'" st)
- st)
- "Syntax table for GraphQL mode.")
-
-(defun graphql-syntax-stringify ()
- "Put `syntax-table' property correctly on single/triple quotes."
- (let* ((ppss (save-excursion (backward-char 3) (syntax-ppss)))
- (string-start (and (eq t (nth 3 ppss)) (nth 8 ppss)))
- (quote-starting-pos (- (point) 3))
- (quote-ending-pos (point)))
- (cond ((or (nth 4 ppss)
- (and string-start
- (not (eql (char-after string-start)
- (char-after quote-starting-pos)))))
- ;; Inside of a comment or a string quoted with different triple
- ;; quotes so do nothing
- nil)
- ((nth 5 ppss)
- ;; The escaped quote - not part of a triple quote
- (goto-char (1+ quote-starting-pos)))
- ((null string-start)
- ;; The start of the string, where we want the string fence syntax on
- ;; the last quote
- (put-text-property (1- quote-ending-pos) quote-ending-pos
- 'syntax-table (string-to-syntax "|")))
- (t
- ;; The end of the string, where we want the string fence syntax on
- ;; the first quote
- (put-text-property quote-starting-pos (1+ quote-starting-pos)
- 'syntax-table (string-to-syntax "|"))))))
-
-(defconst graphql-syntax-propertize-function
- (syntax-propertize-rules
- ("\"\"\"" (0 (ignore (graphql-syntax-stringify))))))
-
-(defvar-local graphql-edit-headers--parent-buffer nil)
-(put 'graphql-edit-headers--parent-buffer 'permanent-local t)
-
-
-(defun graphql-indent-line ()
- "Indent GraphQL schema language."
- (let ((position (point))
- (column)
- (indent-pos))
- (save-excursion
- (graphql-beginning-of-query)
- (setq column (current-column)))
-
- (save-excursion
- (let ((level (car (syntax-ppss (line-beginning-position)))))
-
- ;; Handle closing pairs
- (when (looking-at "\\s-*\\s)")
- (setq level (1- level)))
-
- (indent-line-to (+ column (* graphql-indent-level level)))
- (setq indent-pos (point))))
-
- (when (< position indent-pos)
- (goto-char indent-pos))))
-
-(defvar graphql-keywords
- '("type" "input" "interface" "fragment"
- "query" "enum" "mutation" "subscription"
- "Int" "Float" "String" "Boolean" "ID"
- "true" "false" "null" "extend"
- "scalar" "union"))
-
-(defun graphql-completion-at-point ()
- "Return the list of candidates for completion.
-This is the function to be used for the hook `completion-at-point-functions'."
- (let* ((bds (bounds-of-thing-at-point 'symbol))
- (start (car bds))
- (end (cdr bds)))
- (list start end graphql-keywords . nil)))
-
-
-(defvar graphql-definition-regex
- (concat "\\(" (regexp-opt '("type" "input" "interface" "fragment" "query"
- "mutation" "subscription" "enum" "extend"
- "scalar" "union")) "\\)"
- "[[:space:]]+\\(\\_<.+?\\_>\\)")
- "Keyword Regular Expressions.")
-
-(defvar graphql-builtin-types
- '("Int" "Float" "String" "Boolean" "ID")
- "Built-in GraphQL Types.")
-
-(defvar graphql-constants
- '("true" "false" "null")
- "Constant GraphQL Types.")
-
-
-;;; Check if the point is in an argument list.
-(defun graphql--in-arguments-p ()
- "Return t if the point is in the arguments list of a GraphQL query."
- (let ((opening (cl-second (syntax-ppss))))
- (eql (char-after opening) ?\()))
-
-
-(defun graphql--field-parameter-matcher (limit)
- (catch 'end
- (while t
- (cond
- ;; If we are inside an argument list, try to match the first
- ;; argument that we find or exit the argument list otherwise, so
- ;; the search can continue.
- ((graphql--in-arguments-p)
- (let* ((end (save-excursion (up-list) (point)))
- (match (search-forward-regexp "\\(\\_<[_A-Za-z][_0-9A-Za-z]*?\\_>\\):" end t)))
- (if match
- ;; unless we are inside a string or comment
- (let ((state (syntax-ppss)))
- (when (not (or (nth 3 state)
- (nth 4 state)))
- (throw 'end t)))
- (up-list))))
- (t
- ;; If we are not inside an argument list, jump after the next
- ;; opening parenthesis, and we will try again there.
- (skip-syntax-forward "^(" limit)
- (and (eobp) (throw 'end nil))
- (forward-char))))))
-
-
-(defvar graphql-font-lock-keywords
- `(
- ;; Type definition
- ("\\(type\\)[[:space:]]+\\(\\_<.+?\\_>\\)"
- (1 font-lock-keyword-face)
- (2 font-lock-function-name-face)
- ("[[:space:]]+\\(implements\\)\\(?:[[:space:]]+\\(\\_<.+?\\_>\\)\\)?"
- nil nil
- (1 font-lock-keyword-face)
- (2 font-lock-function-name-face)))
-
- ;; Definitions
- (,graphql-definition-regex
- (1 font-lock-keyword-face)
- (2 font-lock-function-name-face))
-
- ;; Constants
- (,(regexp-opt graphql-constants) . font-lock-constant-face)
-
- ;; Variables
- ("\\$\\_<.+?\\_>" . font-lock-variable-name-face)
-
- ;; Types
- (":[[:space:]]*\\[?\\(\\_<.+?\\_>\\)\\]?"
- (1 font-lock-type-face))
-
- ;; Directives
- ("@\\_<.+?\\_>" . font-lock-keyword-face)
-
- ;; Field parameters
- (graphql--field-parameter-matcher
- (1 font-lock-variable-name-face)))
- "Font Lock keywords.")
-
-;;; Edit headers functionality:
-
-(defun graphql-edit-headers ()
- "Edit graphql request headers interactively in a dedicated buffer.
-
-Open a buffer to edit `graphql-extra-headers'. The contents of this
-buffer take precedence over the setting in `graphql-extra-headers'
-when sending a request."
- (interactive)
- (unless (memq major-mode '(graphql-mode graphql-ts-mode))
- (error "Not in graphql-mode, cannot edit headers"))
- (let ((extra-headers-buffer-name
- (concat "*Graphql Headers for " (buffer-name) "*"))
- (parent-buffer (current-buffer)))
- (pop-to-buffer extra-headers-buffer-name)
- (setq-local graphql-edit-headers--parent-buffer parent-buffer)
- (if (and (string-empty-p (buffer-string)) graphql-extra-headers)
- (progn
- (insert (json-encode graphql-extra-headers))
- (json-pretty-print (point-min) (point-max))
- (goto-char (point-min))))
- (when (fboundp 'json-mode)
- (json-mode))
- (graphql-edit-headers-mode)))
-
-(defun graphql-edit-headers-buffer-p ()
- "Non-nil when current buffer is a header editing buffer."
- (bound-and-true-p graphql-edit-headers-mode))
-
-(defun graphql-edit-headers-accept ()
- "Accept buffer contents and write to `graphql-extra-headers'."
- (interactive)
- (unless (graphql-edit-headers-buffer-p) (error "Not in a GraphQL headers buffer"))
- (let ((new-headers (json-read-from-string (buffer-string))))
- (with-current-buffer graphql-edit-headers--parent-buffer
- (setq-local graphql-extra-headers new-headers)))
- (quit-window 'kill-buffer))
-
-(defun graphql-edit-headers-abort ()
- "Kill current headers buffer and return to graphql file."
- (interactive)
- (unless (graphql-edit-headers-buffer-p) (error "Not in a GraphQL headers buffer"))
- (quit-window 'kill-buffer))
-
-(define-minor-mode graphql-edit-headers-mode
- "Minor mode for editing graphql extra headers.
-\\<graphql-mode-map>
-This minor mode is turned on when you edit GraphQL headers
-interactively with `\\[graphql-edit-headers]'."
- :lighter " GQL Hdr"
- :keymap (let ((map (make-sparse-keymap)))
- (define-key map (kbd "C-c C-c") 'graphql-edit-headers-accept)
- (define-key map (kbd "C-c C-k") 'graphql-edit-headers-abort)
- map)
- (setq header-line-format (substitute-command-keys "Edit GraphQL query headers. Save with \
-`\\[graphql-edit-headers-accept]' or abort with `\\[graphql-edit-headers-abort]'")))
-
-
-;;;###autoload
-(define-derived-mode graphql-mode prog-mode "GraphQL"
- "A major mode to edit GraphQL schemas."
- (setq-local comment-start "# ")
- (setq-local comment-start-skip "#+[\t ]*")
- (setq-local indent-line-function 'graphql-indent-line)
- (setq font-lock-defaults
- `(graphql-font-lock-keywords
- nil
- nil
- nil))
- (setq-local syntax-propertize-function
- graphql-syntax-propertize-function)
- (setq imenu-generic-expression `((nil ,graphql-definition-regex 2)))
- (add-hook 'completion-at-point-functions 'graphql-completion-at-point nil t))
-
-;;;###autoload
-(add-to-list 'auto-mode-alist '("\\.graphql\\'" . graphql-mode))
-;;;###autoload
-(add-to-list 'auto-mode-alist '("\\.gql\\'" . graphql-mode))
-
-
-(provide 'graphql-mode)
-;;; graphql-mode.el ends here
--- /dev/null
+;;; graphql-mode.el --- Major mode for editing GraphQL schemas -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2016, 2017 David Vazquez Pua
+
+;; Author: David Vazquez Pua <davazpNOSPAMgmail.com>
+;; Keywords: languages
+;; Package-Requires: ((emacs "25.1"))
+;; Homepage: https://github.com/davazp/graphql-mode
+;; Package-Version: 20251213.1110
+;; Package-Revision: ccec811d0f1c
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package implements a major mode to edit GraphQL schemas and
+;; query. The basic functionality includes:
+;;
+;; - Syntax highlight
+;; - Automatic indentation
+;;
+;; Additionally, it is able to
+;; - Sending GraphQL queries to an end-point URL
+;; - Uploading files with GraphQL mutations (multipart requests)
+;;
+;; Files with the .graphql and .gql extensions are automatically
+;; opened with this mode.
+
+
+;;; Code:
+
+(require 'newcomment)
+(require 'json)
+(require 'url)
+(require 'cl-lib)
+(require 'let-alist)
+
+;;; User Customizations:
+
+(defgroup graphql nil
+ "Major mode for editing GraphQL schemas and queries."
+ :tag "GraphQL"
+ :group 'languages)
+
+(defcustom graphql-indent-level 2
+ "Number of spaces for each indentation step in `graphql-mode'."
+ :tag "GraphQL"
+ :type 'integer
+ :safe 'integerp
+ :group 'graphql)
+
+(defcustom graphql-url nil
+ "URL address of the graphql server endpoint."
+ :tag "GraphQL"
+ :type 'string
+ :group 'graphql)
+
+(defcustom graphql-variables-file nil
+ "File name containing graphql variables."
+ :tag "GraphQL"
+ :type 'file
+ :group 'graphql)
+
+(defcustom graphql-extra-headers '()
+ "Headers to send to the graphql endpoint."
+ :tag "GraphQL"
+ :type '(repeat sexp)
+ :group 'graphql)
+
+(defcustom graphql-upload-files nil
+ "List of files to upload with GraphQL mutation.
+Each element should be a cons cell (VAR-NAME . FILE-PATH) where
+VAR-NAME is the variable name in the GraphQL query and FILE-PATH
+is the path to the file to upload."
+ :tag "GraphQL"
+ :type '(repeat (cons (string :tag "Variable name")
+ (file :tag "File path")))
+ :group 'graphql)
+
+(defcustom graphql-upload-format 'operations
+ "Format for multipart file upload requests.
+- `operations': Use operations/map format (graphql-multipart-request-spec)
+- `form-data': Use direct form fields (query/variables/map as separate fields)"
+ :tag "GraphQL"
+ :type '(choice (const :tag "Operations format" operations)
+ (const :tag "Form-data format" form-data))
+ :group 'graphql)
+
+(defun graphql-locate-config (dir)
+ "Locate a graphql config starting in DIR."
+ (if-let ((config-dir (locate-dominating-file dir ".graphqlconfig")))
+ (concat config-dir ".graphqlconfig")
+ (error "Could not find a .graphqlconfig file")))
+
+(defun graphql--completing-read-endpoint (endpoints)
+ "Select an endpoint configuration from a list of ENDPOINTS."
+ (completing-read "Select Graphql Endpoint:" (mapcar 'car endpoints)))
+
+(defun graphql-open-config ()
+ "Open the graphql config."
+ (interactive)
+ (find-file (graphql-locate-config ".")))
+
+(defun graphql-select-endpoint ()
+ "Set parameters based off of the endpoints listed in a .graphqlconfig file."
+ (interactive)
+ (let ((config (json-read-file (graphql-locate-config "."))))
+ (let-alist config
+ (if-let ((endpoints .extensions.endpoints)
+ (endpoint (cdr (assq (intern (graphql--completing-read-endpoint endpoints)) endpoints))))
+ (let-alist endpoint
+ (setq graphql-url .url
+ graphql-extra-headers .headers))
+ (error "No endpoint configurations in .graphqlconfig")))))
+
+(defun graphql-encode-json (query &optional operation variables)
+ "Put together a json like object with QUERY, OPERATION, and VARIABLES."
+ (let* ((body '()))
+ (push (cons 'query query) body)
+ (when (and operation (not (string= operation "")))
+ (push (cons 'operationName operation) body))
+ (when variables
+ (push (cons 'variables variables) body))
+ (json-encode body)))
+
+(defun graphql--make-multipart-boundary ()
+ "Generate a unique boundary string for multipart form data."
+ (format "----GraphQLBoundary%s" (md5 (format "%s%s" (current-time) (random)))))
+
+(defun graphql-encode-multipart-operations (query operation variables upload-files boundary)
+ "Encode using operations/map format (graphql-multipart-request-spec).
+
+QUERY is the GraphQL query string.
+OPERATION is the operation name.
+VARIABLES is the variables alist.
+UPLOAD-FILES is a list of (VAR-NAME . FILE-PATH) cons cells.
+BOUNDARY is the multipart boundary string.
+
+Returns the complete multipart body as a unibyte string."
+ (let* ((operations-obj (list (cons 'query query)))
+ (map-obj '())
+ (parts '())
+ (file-index 0))
+
+ ;; Add operation name if provided
+ (when (and operation (not (string= operation "")))
+ (push (cons 'operationName operation) operations-obj))
+
+ ;; Add variables
+ (when variables
+ (push (cons 'variables variables) operations-obj))
+
+ ;; Add operations part
+ (push (format "--%s\r\nContent-Disposition: form-data; name=\"operations\"\r\nContent-Type: application/json\r\n\r\n%s\r\n"
+ boundary
+ (json-encode (nreverse operations-obj)))
+ parts)
+
+ ;; Build the map object
+ (dolist (upload upload-files)
+ (let* ((var-name (car upload))
+ (field-name (format "%d" file-index)))
+ ;; Add to map: {"0": ["variables.varName"]}
+ (push (cons (intern field-name)
+ (vector (format "variables.%s" var-name)))
+ map-obj)
+ (setq file-index (1+ file-index))))
+
+ ;; Add map part
+ (push (format "--%s\r\nContent-Disposition: form-data; name=\"map\"\r\nContent-Type: application/json\r\n\r\n%s\r\n"
+ boundary
+ (json-encode map-obj))
+ parts)
+
+ ;; Add file parts
+ (setq file-index 0)
+ (dolist (upload upload-files)
+ (let* ((file-path (cdr upload))
+ (field-name (format "%d" file-index))
+ (filename (file-name-nondirectory file-path))
+ (file-content (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally file-path)
+ (buffer-string))))
+ (push (format "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: application/octet-stream\r\n\r\n"
+ boundary
+ field-name
+ filename)
+ parts)
+ (push file-content parts)
+ (push "\r\n" parts)
+ (setq file-index (1+ file-index))))
+
+ ;; Add final boundary
+ (push (format "--%s--\r\n" boundary) parts)
+
+ ;; Combine all parts into unibyte string
+ (apply 'concat (nreverse parts))))
+
+(defun graphql-encode-multipart-form-data (query operation variables upload-files boundary)
+ "Encode using direct form-data fields (query/variables/map).
+
+QUERY is the GraphQL query string.
+OPERATION is the operation name.
+VARIABLES is the variables alist.
+UPLOAD-FILES is a list of (VAR-NAME . FILE-PATH) cons cells.
+BOUNDARY is the multipart boundary string.
+
+Returns the complete multipart body as a unibyte string. Equivalent
+to curl -F \\='query=...' -F \\='variables=...\\=' -F \\='map=...' -F \\='file=@...'."
+ (let* ((map-obj '())
+ (parts '()))
+
+ ;; Add query part
+ (push (format "--%s\r\nContent-Disposition: form-data; name=\"query\"\r\n\r\n%s\r\n"
+ boundary
+ query)
+ parts)
+
+ ;; Add operation name if provided
+ (when (and operation (not (string= operation "")))
+ (push (format "--%s\r\nContent-Disposition: form-data; name=\"operationName\"\r\n\r\n%s\r\n"
+ boundary
+ operation)
+ parts))
+
+ ;; Add variables part
+ (when variables
+ (push (format "--%s\r\nContent-Disposition: form-data; name=\"variables\"\r\nContent-Type: application/json\r\n\r\n%s\r\n"
+ boundary
+ (json-encode variables))
+ parts))
+
+ ;; Build the map object and add files
+ (dolist (upload upload-files)
+ (let* ((var-name (car upload))
+ (file-path (cdr upload)))
+ ;; Add to map: {"varName": ["variables.varName"]}
+ (push (cons (intern var-name)
+ (vector (format "variables.%s" var-name)))
+ map-obj)
+
+ ;; Add file part
+ (let* ((filename (file-name-nondirectory file-path))
+ (file-content (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally file-path)
+ (buffer-string))))
+ (push (format "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: application/octet-stream\r\n\r\n"
+ boundary
+ var-name
+ filename)
+ parts)
+ (push file-content parts)
+ (push "\r\n" parts))))
+
+ ;; Add map part
+ (push (format "--%s\r\nContent-Disposition: form-data; name=\"map\"\r\nContent-Type: application/json\r\n\r\n%s\r\n"
+ boundary
+ (json-encode map-obj))
+ parts)
+
+ ;; Add final boundary
+ (push (format "--%s--\r\n" boundary) parts)
+
+ ;; Combine all parts into unibyte string
+ (apply 'concat (nreverse parts))))
+
+(defun graphql-encode-multipart (query operation variables upload-files boundary)
+ "Encode GraphQL request as multipart form data for file uploads.
+
+QUERY is the GraphQL query string.
+OPERATION is the operation name.
+VARIABLES is the variables alist (files should have nil values).
+UPLOAD-FILES is a list of (VAR-NAME . FILE-PATH) cons cells.
+BOUNDARY is the multipart boundary string.
+
+Returns the complete multipart body as a unibyte string.
+Uses format specified by `graphql-upload-format'."
+ (if (eq graphql-upload-format 'form-data)
+ (graphql-encode-multipart-form-data query operation variables upload-files boundary)
+ (graphql-encode-multipart-operations query operation variables upload-files boundary)))
+
+(defun graphql--query (query &optional operation variables upload-files)
+ "Send QUERY to the server and return the response.
+
+The query is sent as a HTTP POST request to the URL at
+`graphql-url'. The query can be any GraphQL definition (query,
+mutation or subscription). OPERATION is a name for the
+operation. VARIABLES is the JSON string that specifies the values
+of the variables used in the query. UPLOAD-FILES is a list of
+\(VAR-NAME . FILE-PATH) cons cells for file uploads."
+ ;; Note that we need to get the value of graphql-url in the current
+ ;; before before we switch to the temporary one.
+ (let ((url graphql-url))
+ (graphql-post-request url query operation variables upload-files)))
+
+(declare-function request "request")
+(declare-function request-response-data "request")
+(declare-function request-response--raw-header "request")
+
+(defun graphql-post-request (url query &optional operation variables upload-files)
+ "Make post request to graphql server with url and body.
+
+URL hostname, path, search parameters, such as operationName and variables
+QUERY query definition(s) of query, mutation, and/or subscription
+OPERATION name of the operation if multiple definition is given in QUERY
+VARIABLES list of variables for query operation
+UPLOAD-FILES list of (VAR-NAME . FILE-PATH) cons cells for file uploads"
+ (or (require 'request nil t)
+ (error "graphql-post-request needs the request package. \
+Please install it and try again."))
+ (if upload-files
+ ;; Multipart request for file uploads
+ (let* ((boundary (graphql--make-multipart-boundary))
+ (body (graphql-encode-multipart query operation variables upload-files boundary))
+ (headers (append `(("Content-Type" . ,(format "multipart/form-data; boundary=%s" boundary)))
+ graphql-extra-headers)))
+ (request url
+ :type "POST"
+ :data body
+ :headers headers
+ :parser 'json-read
+ :sync t
+ :complete (lambda (&rest _)
+ (message "%s" (if (string-equal "" operation)
+ url
+ (format "%s?operationName=%s"
+ url operation))))))
+ ;; Regular JSON request
+ (let* ((body (graphql-encode-json query operation variables))
+ (headers (append '(("Content-Type" . "application/json")) graphql-extra-headers)))
+ (request url
+ :type "POST"
+ :data body
+ :headers headers
+ :parser 'json-read
+ :sync t
+ :complete (lambda (&rest _)
+ (message "%s" (if (string-equal "" operation)
+ url
+ (format "%s?operationName=%s"
+ url operation))))))))
+
+(defun graphql-beginning-of-query ()
+ "Move the point to the beginning of the current query."
+ (interactive)
+ (goto-char (or (syntax-ppss-toplevel-pos (syntax-ppss (line-beginning-position)))
+ (line-beginning-position)))
+ (back-to-indentation))
+
+(defun graphql-end-of-query ()
+ "Move the point to the end of the current query."
+ (interactive)
+ (while (and (< (point) (point-max))
+ (or (> (current-indentation) 0)
+ (> (car (syntax-ppss)) 0)))
+ (forward-line 1)))
+
+(defun graphql-current-query ()
+ "Return the current query/mutation/subscription definition."
+ (let ((start
+ (save-excursion
+ (graphql-beginning-of-query)
+ (point)))
+ (end
+ (save-excursion
+ (graphql-end-of-query)
+ (point))))
+ (if (not (equal start end))
+ (buffer-substring-no-properties start end)
+ (save-excursion
+ (let ((saved-point (point))
+ (line (thing-at-point 'line t)))
+ (when (string-match-p (regexp-quote "}") line)
+ (search-backward "}" (beginning-of-line)))
+ (when (string-match-p (regexp-quote "{") line)
+ (search-forward "{" (end-of-line)))
+ (if (= (point) saved-point)
+ nil
+ (graphql-current-query)))))))
+
+(defun graphql-current-operation ()
+ "Return the name of the current graphql query."
+ (let* ((query
+ (save-excursion
+ (replace-regexp-in-string "^[ \t\n]*" ""
+ (or (graphql-current-query) ""))))
+ (tokens
+ (split-string query "[ \f\t\n\r\v]+"))
+ (first (nth 0 tokens)))
+
+ (if (or (string-equal first "{") (string-equal first ""))
+ nil
+ (replace-regexp-in-string "[({].*" "" (nth 1 tokens)))))
+
+(defun graphql-current-variables (filename)
+ "Return the current variables contained in FILENAME."
+ (if (and filename
+ (not (string-equal filename ""))
+ (not (file-directory-p filename))
+ (file-exists-p filename))
+ (condition-case nil
+ (progn (get-buffer-create (find-file-noselect filename))
+ (json-read-file filename))
+ (error nil))
+ nil))
+
+(define-minor-mode graphql-query-response-mode
+ "Allows GraphQL query response buffer to be closed with (q)"
+ :lighter " GraphQL Response"
+ :keymap (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "q") 'quit-window)
+ map))
+
+(defun graphql-read-upload-files ()
+ "Interactively read files to upload with GraphQL query.
+Returns a list of (VAR-NAME . FILE-PATH) cons cells."
+ (let ((files '())
+ (continue t))
+ (while continue
+ (let ((var-name (read-string "Variable name (empty to finish): ")))
+ (if (string-empty-p var-name)
+ (setq continue nil)
+ (let ((file-path (read-file-name (format "File for %s: " var-name))))
+ (push (cons var-name file-path) files)))))
+ (nreverse files)))
+
+(defun graphql-send-query (&optional prompt)
+ "Send the current GraphQL query/mutation/subscription to server.
+With \\[universal-argument] PROMPT, prompt for
+`graphql-url'/`graphql-variables-file'/`graphql-upload-files'."
+ (interactive "P")
+ (let* ((url (or (and (not prompt) graphql-url)
+ (read-string "GraphQL URL: " graphql-url)))
+ (var (or (and (not prompt) graphql-variables-file)
+ (read-file-name "GraphQL Variables: " nil graphql-variables-file)))
+ (files (if prompt
+ (when (y-or-n-p "Upload files? ")
+ (graphql-read-upload-files))
+ graphql-upload-files)))
+ (let ((graphql-url url)
+ (graphql-variables-file var)
+ (graphql-upload-files files))
+
+ (let* ((query (buffer-substring-no-properties (point-min) (point-max)))
+ (operation (graphql-current-operation))
+ (variables (graphql-current-variables var))
+ (response (graphql--query query operation variables files)))
+ (with-current-buffer-window
+ "*GraphQL*" 'display-buffer-pop-up-window nil
+ (erase-buffer)
+ (when (fboundp 'json-mode)
+ (json-mode))
+ (insert (json-encode (request-response-data response)))
+ (json-pretty-print-buffer)
+ (goto-char (point-max))
+ (insert "\n\n"
+ (propertize (request-response--raw-header response)
+ 'face 'font-lock-comment-face
+ 'font-lock-face 'font-lock-comment-face))
+ (graphql-query-response-mode))))
+ ;; If the query was successful, then save the value of graphql-url
+ ;; in the current buffer (instead of the introduced local
+ ;; binding).
+ (setq graphql-url url)
+ (setq graphql-variables-file var)
+ (setq graphql-upload-files files)
+ nil))
+
+(defvar graphql-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-c C-c") 'graphql-send-query)
+ (define-key map (kbd "C-c C-l") 'graphql-select-endpoint)
+ (define-key map (kbd "C-c e h") 'graphql-edit-headers)
+ map)
+ "Key binding for GraphQL mode.")
+
+(defvar graphql-mode-syntax-table
+ (let ((st (make-syntax-table)))
+ (modify-syntax-entry ?\# "<" st)
+ (modify-syntax-entry ?\n ">" st)
+ (modify-syntax-entry ?\$ "'" st)
+ st)
+ "Syntax table for GraphQL mode.")
+
+(defun graphql-syntax-stringify ()
+ "Put `syntax-table' property correctly on single/triple quotes."
+ (let* ((ppss (save-excursion (backward-char 3) (syntax-ppss)))
+ (string-start (and (eq t (nth 3 ppss)) (nth 8 ppss)))
+ (quote-starting-pos (- (point) 3))
+ (quote-ending-pos (point)))
+ (cond ((or (nth 4 ppss)
+ (and string-start
+ (not (eql (char-after string-start)
+ (char-after quote-starting-pos)))))
+ ;; Inside of a comment or a string quoted with different triple
+ ;; quotes so do nothing
+ nil)
+ ((nth 5 ppss)
+ ;; The escaped quote - not part of a triple quote
+ (goto-char (1+ quote-starting-pos)))
+ ((null string-start)
+ ;; The start of the string, where we want the string fence syntax on
+ ;; the last quote
+ (put-text-property (1- quote-ending-pos) quote-ending-pos
+ 'syntax-table (string-to-syntax "|")))
+ (t
+ ;; The end of the string, where we want the string fence syntax on
+ ;; the first quote
+ (put-text-property quote-starting-pos (1+ quote-starting-pos)
+ 'syntax-table (string-to-syntax "|"))))))
+
+(defconst graphql-syntax-propertize-function
+ (syntax-propertize-rules
+ ("\"\"\"" (0 (ignore (graphql-syntax-stringify))))))
+
+(defvar-local graphql-edit-headers--parent-buffer nil)
+(put 'graphql-edit-headers--parent-buffer 'permanent-local t)
+
+
+(defun graphql-indent-line ()
+ "Indent GraphQL schema language."
+ (let ((position (point))
+ (column)
+ (indent-pos))
+ (save-excursion
+ (graphql-beginning-of-query)
+ (setq column (current-column)))
+
+ (save-excursion
+ (let ((level (car (syntax-ppss (line-beginning-position)))))
+
+ ;; Handle closing pairs
+ (when (looking-at "\\s-*\\s)")
+ (setq level (1- level)))
+
+ (indent-line-to (+ column (* graphql-indent-level level)))
+ (setq indent-pos (point))))
+
+ (when (< position indent-pos)
+ (goto-char indent-pos))))
+
+(defvar graphql-keywords
+ '("type" "input" "interface" "fragment"
+ "query" "enum" "mutation" "subscription"
+ "Int" "Float" "String" "Boolean" "ID"
+ "true" "false" "null" "extend"
+ "scalar" "union"))
+
+(defun graphql-completion-at-point ()
+ "Return the list of candidates for completion.
+This is the function to be used for the hook `completion-at-point-functions'."
+ (let* ((bds (bounds-of-thing-at-point 'symbol))
+ (start (car bds))
+ (end (cdr bds)))
+ (list start end graphql-keywords . nil)))
+
+
+(defvar graphql-definition-regex
+ (concat "\\(" (regexp-opt '("type" "input" "interface" "fragment" "query"
+ "mutation" "subscription" "enum" "extend"
+ "scalar" "union")) "\\)"
+ "[[:space:]]+\\(\\_<.+?\\_>\\)")
+ "Keyword Regular Expressions.")
+
+(defvar graphql-builtin-types
+ '("Int" "Float" "String" "Boolean" "ID")
+ "Built-in GraphQL Types.")
+
+(defvar graphql-constants
+ '("true" "false" "null")
+ "Constant GraphQL Types.")
+
+
+;;; Check if the point is in an argument list.
+(defun graphql--in-arguments-p ()
+ "Return t if the point is in the arguments list of a GraphQL query."
+ (let ((opening (cl-second (syntax-ppss))))
+ (eql (char-after opening) ?\()))
+
+
+(defun graphql--field-parameter-matcher (limit)
+ (catch 'end
+ (while t
+ (cond
+ ;; If we are inside an argument list, try to match the first
+ ;; argument that we find or exit the argument list otherwise, so
+ ;; the search can continue.
+ ((graphql--in-arguments-p)
+ (let* ((end (save-excursion (up-list) (point)))
+ (match (search-forward-regexp "\\(\\_<[_A-Za-z][_0-9A-Za-z]*?\\_>\\):" end t)))
+ (if match
+ ;; unless we are inside a string or comment
+ (let ((state (syntax-ppss)))
+ (when (not (or (nth 3 state)
+ (nth 4 state)))
+ (throw 'end t)))
+ (up-list))))
+ (t
+ ;; If we are not inside an argument list, jump after the next
+ ;; opening parenthesis, and we will try again there.
+ (skip-syntax-forward "^(" limit)
+ (and (eobp) (throw 'end nil))
+ (forward-char))))))
+
+
+(defvar graphql-font-lock-keywords
+ `(
+ ;; Type definition
+ ("\\(type\\)[[:space:]]+\\(\\_<.+?\\_>\\)"
+ (1 font-lock-keyword-face)
+ (2 font-lock-function-name-face)
+ ("[[:space:]]+\\(implements\\)\\(?:[[:space:]]+\\(\\_<.+?\\_>\\)\\)?"
+ nil nil
+ (1 font-lock-keyword-face)
+ (2 font-lock-function-name-face)))
+
+ ;; Definitions
+ (,graphql-definition-regex
+ (1 font-lock-keyword-face)
+ (2 font-lock-function-name-face))
+
+ ;; Constants
+ (,(regexp-opt graphql-constants) . font-lock-constant-face)
+
+ ;; Variables
+ ("\\$\\_<.+?\\_>" . font-lock-variable-name-face)
+
+ ;; Types
+ (":[[:space:]]*\\[?\\(\\_<.+?\\_>\\)\\]?"
+ (1 font-lock-type-face))
+
+ ;; Directives
+ ("@\\_<.+?\\_>" . font-lock-keyword-face)
+
+ ;; Field parameters
+ (graphql--field-parameter-matcher
+ (1 font-lock-variable-name-face)))
+ "Font Lock keywords.")
+
+;;; Edit headers functionality:
+
+(defun graphql-edit-headers ()
+ "Edit graphql request headers interactively in a dedicated buffer.
+
+Open a buffer to edit `graphql-extra-headers'. The contents of this
+buffer take precedence over the setting in `graphql-extra-headers'
+when sending a request."
+ (interactive)
+ (unless (memq major-mode '(graphql-mode graphql-ts-mode))
+ (error "Not in graphql-mode, cannot edit headers"))
+ (let ((extra-headers-buffer-name
+ (concat "*Graphql Headers for " (buffer-name) "*"))
+ (parent-buffer (current-buffer)))
+ (pop-to-buffer extra-headers-buffer-name)
+ (setq-local graphql-edit-headers--parent-buffer parent-buffer)
+ (if (and (string-empty-p (buffer-string)) graphql-extra-headers)
+ (progn
+ (insert (json-encode graphql-extra-headers))
+ (json-pretty-print (point-min) (point-max))
+ (goto-char (point-min))))
+ (when (fboundp 'json-mode)
+ (json-mode))
+ (graphql-edit-headers-mode)))
+
+(defun graphql-edit-headers-buffer-p ()
+ "Non-nil when current buffer is a header editing buffer."
+ (bound-and-true-p graphql-edit-headers-mode))
+
+(defun graphql-edit-headers-accept ()
+ "Accept buffer contents and write to `graphql-extra-headers'."
+ (interactive)
+ (unless (graphql-edit-headers-buffer-p) (error "Not in a GraphQL headers buffer"))
+ (let ((new-headers (json-read-from-string (buffer-string))))
+ (with-current-buffer graphql-edit-headers--parent-buffer
+ (setq-local graphql-extra-headers new-headers)))
+ (quit-window 'kill-buffer))
+
+(defun graphql-edit-headers-abort ()
+ "Kill current headers buffer and return to graphql file."
+ (interactive)
+ (unless (graphql-edit-headers-buffer-p) (error "Not in a GraphQL headers buffer"))
+ (quit-window 'kill-buffer))
+
+(define-minor-mode graphql-edit-headers-mode
+ "Minor mode for editing graphql extra headers.
+\\<graphql-mode-map>
+This minor mode is turned on when you edit GraphQL headers
+interactively with `\\[graphql-edit-headers]'."
+ :lighter " GQL Hdr"
+ :keymap (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-c C-c") 'graphql-edit-headers-accept)
+ (define-key map (kbd "C-c C-k") 'graphql-edit-headers-abort)
+ map)
+ (setq header-line-format (substitute-command-keys "Edit GraphQL query headers. Save with \
+`\\[graphql-edit-headers-accept]' or abort with `\\[graphql-edit-headers-abort]'")))
+
+
+;;;###autoload
+(define-derived-mode graphql-mode prog-mode "GraphQL"
+ "A major mode to edit GraphQL schemas."
+ (setq-local comment-start "# ")
+ (setq-local comment-start-skip "#+[\t ]*")
+ (setq-local indent-line-function 'graphql-indent-line)
+ (setq font-lock-defaults
+ `(graphql-font-lock-keywords
+ nil
+ nil
+ nil))
+ (setq-local syntax-propertize-function
+ graphql-syntax-propertize-function)
+ (setq imenu-generic-expression `((nil ,graphql-definition-regex 2)))
+ (add-hook 'completion-at-point-functions 'graphql-completion-at-point nil t))
+
+;;;###autoload
+(add-to-list 'auto-mode-alist '("\\.graphql\\'" . graphql-mode))
+;;;###autoload
+(add-to-list 'auto-mode-alist '("\\.gql\\'" . graphql-mode))
+
+
+(provide 'graphql-mode)
+;;; graphql-mode.el ends here