| 0:
124 | argumentref = argumentrefs[0]
125 | ul = get_sibling(argumentref)
126 | while ul is not None:
127 | if ul.name == 'ul':
128 | lis = ul.find_all('li', recursive=False)
129 | for li in lis:
130 | codes = li.find_all('code')
131 | if len(codes) > 0:
132 | argname = codes[0].get_text()
133 | else:
134 | aas = li.find_all('a')
135 | if len(aas) is 0:
136 | continue
137 | if aas[0].get('name') is not None:
138 | argname = aas[0]['name']
139 | else:
140 | argname = li.get_text().split('-')[0].strip()
141 | argdoc = li.get_text().replace("\n", " ").strip()
142 | arglist.append({'name': argname, 'doc': argdoc.translate(escaping)})
143 | break
144 | if ul.name == 'h2':
145 | break
146 | ul = get_sibling(ul)
147 | return arglist
148 |
149 |
150 | def get_resource_params(name, docpath):
151 | blacklist = [
152 | 'vault_aws_auth_backend_role_tag' # This resource has a completely
153 | # broken docpage.
154 | ]
155 | if name in blacklist:
156 | return
157 | page = s.get("https://www.terraform.io%s" % docpath)
158 | kind = {'r': 'resource', 'd': 'data', 'external': 'data', 'http': 'data'}[docpath.split('/')[-2]]
159 | assert(page.status_code is 200)
160 | soup = BeautifulSoup(page.content, 'html.parser')
161 | headers = soup.find_all('h1', id=name)
162 | if len(headers) is 0:
163 | # Sometimes docs are bonkers and use a wring header.
164 | headers = soup.find_all('h1')
165 | header = headers[0]
166 | maindoc = ""
167 | p = get_sibling(header)
168 | while p and p.name != 'h2':
169 | maindoc += p.get_text().replace("\n", " ") + "\n\n"
170 | p = get_sibling(p)
171 | maindoc = maindoc.strip()
172 |
173 | arglist = arghelper('argument', soup)
174 | attrlist = arghelper('attributes', soup)
175 |
176 | return {
177 | 'name': name,
178 | 'doc': maindoc.translate(escaping),
179 | 'args': arglist,
180 | 'attrs': attrlist,
181 | 'type': kind
182 | }
183 |
184 | def get_interpolation_functions():
185 | page = s.get("https://www.terraform.io/docs/configuration/interpolation.html")
186 | soup = BeautifulSoup(page.content, 'html.parser')
187 | header = soup.find_all('h2', id='built-in-functions')[0]
188 | ul = get_sibling(header)
189 | while ul.name != 'ul':
190 | ul = get_sibling(ul)
191 | lis = ul.find_all('li', recursive=False)
192 | for li in lis:
193 | aas = li.find_all('a')
194 | if len(aas) is 0:
195 | continue
196 | funcsig = aas[1].get_text()
197 | funcname = funcsig.split("(")[0]
198 | funcparam = funcsig[len(funcname):]
199 | funcdoc = li.get_text().replace("\n", " ").strip()
200 | functions_list.append({'name': funcname, 'doc': funcdoc.translate(escaping), 'param': funcparam})
201 |
202 |
203 | def gather_all_by_provider(provider):
204 | global resource_list
205 | print("Gathering resources for %s" % provider)
206 | resources = get_resources_by_provider(provider)
207 | for name, docpath in resources:
208 | print("- " + name)
209 | params = get_resource_params(name, docpath)
210 | if params is not None:
211 | resource_list.append(params)
212 |
213 |
214 | def prepare_file():
215 | print("Generating file...")
216 | data = ""
217 |
218 | # First, build up bare resource list.
219 | reslist = ""
220 | for resource in resource_list:
221 | if resource['type'] == 'resource':
222 | reslist += " (\"%(name)s\" \"%(doc)s\")\n" % {
223 | 'name': resource['name'],
224 | 'doc': resource['doc']}
225 | data += "(defconst company-terraform-resources-list '(\n" + reslist + " ))\n\n"
226 |
227 | # Then, resource argument hashes.
228 | for resource in resource_list:
229 | if resource['type'] == 'resource':
230 | arglist = ""
231 | for argument in resource['args']:
232 | arglist += " (\"%(name)s\" \"%(doc)s\")\n" % {
233 | 'name': argument['name'],
234 | 'doc': argument['doc']}
235 | data += "(puthash \"%(name)s\" '(\n%(arglist)s\n ) company-terraform-resource-arguments-hash)\n\n" % {
236 | 'name': resource['name'],
237 | 'arglist': arglist}
238 |
239 | # Then, resource attribute hashes.
240 | for resource in resource_list:
241 | if resource['type'] == 'resource':
242 | attrlist = ""
243 | for attribute in resource['attrs']:
244 | attrlist += " (\"%(name)s\" \"%(doc)s\")\n" % {
245 | 'name':attribute['name'],
246 | 'doc': attribute['doc']}
247 | data += "(puthash \"%(name)s\" '(\n%(attrlist)s\n ) company-terraform-resource-attributes-hash)\n\n" % {
248 | 'name': resource['name'],
249 | 'attrlist': attrlist}
250 |
251 | # Data list.
252 | reslist = ""
253 | for resource in resource_list:
254 | if resource['type'] == 'data':
255 | reslist += " (\"%(name)s\" \"%(doc)s\")\n" % {
256 | 'name': resource['name'],
257 | 'doc': resource['doc']}
258 | data += "(defconst company-terraform-data-list '(\n" + reslist + " ))\n\n"
259 |
260 | # Then, data argument hashes.
261 | for resource in resource_list:
262 | if resource['type'] == 'data':
263 | arglist = ""
264 | for argument in resource['args']:
265 | arglist += " (\"%(name)s\" \"%(doc)s\")\n" % {
266 | 'name': argument['name'],
267 | 'doc': argument['doc']}
268 | data += "(puthash \"%(name)s\" '(\n%(arglist)s\n ) company-terraform-data-arguments-hash)\n\n" % {
269 | 'name': resource['name'],
270 | 'arglist': arglist}
271 |
272 | # Then, data attribute hashes.
273 | for resource in resource_list:
274 | if resource['type'] == 'data':
275 | attrlist = ""
276 | for attribute in resource['attrs']:
277 | attrlist += " (\"%(name)s\" \"%(doc)s\")\n" % {
278 | 'name':attribute['name'],
279 | 'doc': attribute['doc']}
280 | data += "(puthash \"%(name)s\" '(\n%(attrlist)s\n ) company-terraform-data-attributes-hash)\n\n" % {
281 | 'name': resource['name'],
282 | 'attrlist': attrlist}
283 |
284 | funclist = ""
285 | for func in functions_list:
286 | funclist += " (\"%(name)s\" \"%(doc)s\")\n" % {
287 | 'name':func['name'],
288 | 'doc': func['doc']}
289 | data += "(defconst company-terraform-interpolation-functions '(\n" + funclist + " ))\n\n"
290 |
291 | with open("company-terraform-data.el", "w") as file:
292 | file.write(header + data + footer)
293 |
294 | get_providers()
295 | for p in provider_list:
296 | gather_all_by_provider(p)
297 | get_interpolation_functions()
298 | prepare_file()
299 |
--------------------------------------------------------------------------------
/company-terraform.el:
--------------------------------------------------------------------------------
1 | ;;; company-terraform.el --- A company backend for terraform
2 |
3 | ;; Copyright (C) 2017 Rafał Cieślak
4 |
5 | ;; Author: Rafał Cieślak
6 | ;; Version: 1.0
7 | ;; Package-Requires: ((emacs "24.4") (company "0.8.12") (terraform-mode "0.06"))
8 | ;; Created: 10 August 2017
9 | ;; Keywords: abbrev, convenience, terraform, company
10 | ;; URL: https://github.com/rafalcieslak/emacs-company-terraform
11 |
12 | ;;; Commentary:
13 |
14 | ;; company-terraform provides a company backend for terraform files. It enables
15 | ;; context-aware autocompletion for terraform sources. This includes resource
16 | ;; and data arguments and attributes, both in resource and data blocks as well
17 | ;; as in interpolations, built-in functions and top-level keywords.
18 |
19 | ;;; Code:
20 |
21 | (require 'company)
22 | (require 'cl-lib)
23 | (require 'subr-x)
24 | (require 'terraform-mode)
25 |
26 | (require 'company-terraform-data)
27 |
28 | (defun company-terraform--scan-resources (dir)
29 | "Search .tf files in DIR for resource data and variable blocks."
30 | (let* ((files (directory-files dir t "\\.tf$"))
31 | (datas (make-hash-table :test 'equal))
32 | (resources (make-hash-table :test 'equal))
33 | (variables '())
34 | (outputs '())
35 | (locals '())
36 | (modules '())
37 | (modules-with-dirs (make-hash-table :test 'equal)))
38 | (dolist (file files)
39 | (with-temp-buffer
40 | (if (find-buffer-visiting file)
41 | ;; If this file is being edited, use the current (possibly unsaved) version.
42 | (insert (with-current-buffer (find-buffer-visiting file) (buffer-string)))
43 | ;; Otherwise just open the file from file system.
44 | (ignore-errors (insert-file-contents file)))
45 | (goto-char 1) ; Start by searching for data and resource blocks.
46 | (while (re-search-forward "\\(resource\\|data\\)[[:space:]\n]*\"\\([^\"]*\\)\"[[:space:]\n]*\"\\([^\"]*\\)\"[[:space:]\n]*{" nil t)
47 | (let* ((kind (intern (match-string-no-properties 1)))
48 | (hash-to-use (cl-case kind
49 | ('data datas)
50 | ('resource resources)))
51 | (type (match-string-no-properties 2))
52 | (name (match-string-no-properties 3)))
53 | (when (eq 'empty (gethash type hash-to-use 'empty))
54 | (puthash type '() hash-to-use))
55 | (push name (gethash type hash-to-use))))
56 | (goto-char 1) ; Then search for variable blocks.
57 | (while (re-search-forward "variable[[:space:]\n]*\"\\([^\"]*\\)\"[[:space:]\n]*{" nil t)
58 | (push (match-string-no-properties 1) variables))
59 | (goto-char 1) ; Then search for output blocks.
60 | (while (re-search-forward "output[[:space:]\n]*\"\\([^\"]*\\)\"[[:space:]\n]*{" nil t)
61 | (push (match-string-no-properties 1) outputs))
62 | (goto-char 1) ; Then search for locals
63 | (while (re-search-forward "locals[[:space:]\n]*{" nil t)
64 | (let ((end (save-excursion (backward-char) (forward-sexp) (point))))
65 | ;; TODO: This will also find sub-keys for locals which are nested dicts.
66 | (while (re-search-forward "\n[[:space:]]*\\([^[:space:]\n#]*\\)[[:space:]]*=" end t)
67 | (push (match-string-no-properties 1) locals))
68 | ))
69 | (goto-char 1) ; Then search for modules
70 | (while (re-search-forward "module[[:space:]\n]*\"\\([^\"]*\\)\"[[:space:]\n]*{" nil t)
71 | (let ((module-name (match-string-no-properties 1))
72 | (end (save-excursion (backward-char) (forward-sexp) (point))))
73 | (push module-name modules)
74 | ;; Search for module source path
75 | (while (re-search-forward "\n[[:space:]]*source[[:space:]]*=[[:space:]]*\"\\([^\"]*\\)\"" end t)
76 | (let* ((module-dir-hash (secure-hash 'md5 (concat "1." module-name ";" (match-string-no-properties 1))))
77 | (module-dir (concat dir ".terraform/modules/" module-dir-hash)))
78 | ;; If the dir does not exist, use data straight from source dir
79 | (if (file-directory-p module-dir)
80 | (puthash module-name module-dir modules-with-dirs)
81 | (puthash module-name (concat dir (match-string-no-properties 1)) modules-with-dirs))))
82 | ))))
83 | (list datas resources variables outputs locals modules modules-with-dirs)))
84 |
85 | (defconst company-terraform-perdir-resource-cache
86 | (make-hash-table :test 'equal))
87 |
88 | (defun company-terraform-get-resource-cache (kind &optional dir)
89 | "Return several dictionaries gathering names used in the project.
90 | KIND specifies the block type requested and mey be 'resource,
91 | 'data or 'variable. Searches for blocks in DIR or buffer's
92 | directory if DIR is nil. If available, uses a cached version
93 | which lasts serval seconds."
94 | (nth (cl-case kind
95 | ('data 0)
96 | ('resource 1)
97 | ('variable 2)
98 | ('output 3)
99 | ('local 4)
100 | ('module 5)
101 | ('module-dir 6))
102 | (let* ((dir (or dir (file-name-directory (buffer-file-name))))
103 | (v (gethash dir company-terraform-perdir-resource-cache))
104 | (cache-time (car v))
105 | (resource-data (cdr v)))
106 | (if (and v
107 | (< (- (float-time) cache-time) 20))
108 | resource-data
109 | (progn
110 | (message "Regenerating company-terraform resource cache for %s..." dir)
111 | (let ((resource-data (company-terraform--scan-resources dir)))
112 | (puthash dir (cons (float-time) resource-data) company-terraform-perdir-resource-cache)
113 | resource-data))))))
114 |
115 | (defun company-terraform-get-context ()
116 | "Guess the context in terraform description where point is."
117 | (let ((nest-level (nth 0 (syntax-ppss)))
118 | (curr-ppos (nth 1 (syntax-ppss)))
119 | (string-state (nth 3 (syntax-ppss)))
120 | (string-ppos (nth 8 (syntax-ppss))))
121 | (cond
122 | ;; Resource/data type
123 | ((and string-state
124 | (save-excursion
125 | (goto-char string-ppos)
126 | (re-search-backward "\\(resource\\|data\\)[[:space:]\n]*\\=" nil t)))
127 | (list 'object-type (intern (match-string-no-properties 1))))
128 | ((or
129 | ;; String interpolation
130 | (and (> nest-level 0)
131 | string-state
132 | (save-excursion
133 | (re-search-backward "\\${[^\"]*\\=" nil t)))
134 | ;; Assignment expression
135 | (and (> nest-level 0)
136 | (save-excursion
137 | (re-search-backward "=[^\n=\"]*\\=" nil t)))
138 | )
139 | (list 'interpolation
140 | (buffer-substring
141 | (point)
142 | (save-excursion
143 | (with-syntax-table (make-syntax-table (syntax-table))
144 | ;; Minus, asterisk and dot characters are part of the object path.
145 | (modify-syntax-entry ?- "w")
146 | (modify-syntax-entry ?. "w")
147 | (modify-syntax-entry ?* "w")
148 | (skip-syntax-backward "w")
149 | (point))))))
150 | ;; Inside resource/data block
151 | ((and (eq ?{ (char-after curr-ppos))
152 | (save-excursion
153 | (goto-char curr-ppos)
154 | (re-search-backward "\\(resource\\|data\\|module\\)[[:space:]\n]*\"\\([^\"]*\\)\"[[:space:]\n]*\\(\"[^\"]*\"[[:space:]\n]*\\)?\\=" nil t)))
155 |
156 | (list 'block (intern (match-string-no-properties 1)) (match-string-no-properties 2)))
157 | ;; Top level
158 | ((eq 0 nest-level) 'top-level)
159 | (t 'no-idea))))
160 |
161 | (defun company-terraform-test-context ()
162 | "Echoes a message naming the current context in a terraform file. Useful for diagnostics."
163 | (interactive)
164 | (message "company-terraform-context: %s" (company-terraform-get-context)))
165 |
166 | (defun company-terraform--prefix ()
167 | "Return the text before point that is part of a completable symbol.
168 | Check function ‘company-mode’ docs for the details on how this
169 | function's result is interpreted."
170 | (if (eq major-mode 'terraform-mode)
171 | (let ((context (company-terraform-get-context)))
172 | (pcase context
173 | ('no-idea nil)
174 | ('top-level (company-grab-symbol))
175 | (`(interpolation . ,_) (cons (car (last (split-string (nth 1 context) "\\."))) t))
176 | (`(object-type . ,_) (company-grab-symbol-cons "\"" 1))
177 | (`(resource . ,_) (company-grab-symbol))
178 | (`(data . ,_) (company-grab-symbol))
179 | (_ (company-grab-symbol))))))
180 |
181 | (defun company-terraform--make-candidate (candidate)
182 | "Annotates a completion suggestion from a name-doc list CANDIDATE."
183 | (let ((text (nth 0 candidate))
184 | (doc (nth 1 candidate)))
185 | (propertize text 'doc doc)))
186 |
187 | (defun company-terraform--filterdoc (prefix lists &optional multi)
188 | "Filters candidates for a PREFIX.
189 | The candidates are provided either as a single list of a list of
190 | LISTS if MULTI is non-nil. Each candidate is either a single
191 | string of a pair of string and documentation."
192 | (if (not multi) (setq lists (list lists)))
193 | (cl-loop
194 | for l in lists
195 | append (cl-loop
196 | for item in l
197 | if (and (stringp item) (string-prefix-p prefix item))
198 | collect item
199 | else if (and (listp item) (string-prefix-p prefix (car item)))
200 | collect (company-terraform--make-candidate item))))
201 |
202 | (defun company-terraform-is-resource-n (string)
203 | "True iff STRING is an integer or a literal * character."
204 | (if (string-match "\\`\\([0-9]+\\)\\|*\\'" string) t nil))
205 |
206 | (defun company-terraform-candidates (prefix)
207 | "Prepare a list of autocompletion candidates for the given PREFIX."
208 | (let ((context (company-terraform-get-context)))
209 | (pcase context
210 | ('top-level
211 | (company-terraform--filterdoc prefix company-terraform-toplevel-keywords))
212 | (`(object-type resource)
213 | (company-terraform--filterdoc prefix company-terraform-resources-list))
214 | (`(object-type data)
215 | (company-terraform--filterdoc prefix company-terraform-data-list))
216 | (`(block resource ,type)
217 | (company-terraform--filterdoc prefix
218 | (list (gethash type company-terraform-resource-arguments-hash)
219 | company-terraform-resource-extra)
220 | t))
221 | (`(block data ,type)
222 | (company-terraform--filterdoc prefix
223 | (list (gethash type company-terraform-data-arguments-hash)
224 | company-terraform-data-extra)
225 | t))
226 | (`(block module ,module-name)
227 | (company-terraform--filterdoc prefix (company-terraform-get-resource-cache
228 | 'variable
229 | (gethash module-name (company-terraform-get-resource-cache 'module-dir)))))
230 | (`(interpolation ,pathstr)
231 | ;; Within interpolation
232 | (pcase (split-string pathstr "\\.")
233 | (`(,x)
234 | ;; Complete function name or resource type.
235 | (company-terraform--filterdoc x
236 | (list company-terraform-interpolation-functions
237 | (hash-table-keys (company-terraform-get-resource-cache 'resource))
238 | company-terraform-interpolation-extra)
239 | t))
240 | (`("count" ,x)
241 | ;; Complete count metadata
242 | (company-terraform--filterdoc x company-terraform-count-extra))
243 | (`("var" ,x)
244 | ;; Complete variable name.
245 | (company-terraform--filterdoc x (company-terraform-get-resource-cache 'variable)))
246 | (`("local" ,x)
247 | ;; Complete locals name.
248 | (company-terraform--filterdoc x (company-terraform-get-resource-cache 'local)))
249 | (`("module" ,x)
250 | ;; Complete module name.
251 | (company-terraform--filterdoc x (company-terraform-get-resource-cache 'module)))
252 | (`("data" ,x)
253 | ;; Complete data source type.
254 | (company-terraform--filterdoc x (hash-table-keys (company-terraform-get-resource-cache 'data))))
255 | (`("data" ,data-type ,x)
256 | ;; Complete data name.
257 | (company-terraform--filterdoc x
258 | (gethash data-type (company-terraform-get-resource-cache 'data))))
259 | (`("data" ,data-type ,data-name . ,(or `(,x)
260 | `(,(pred company-terraform-is-resource-n) ,x)))
261 | ;; Complete data arguments/attributes
262 | (company-terraform--filterdoc x
263 | (list (gethash data-type company-terraform-data-arguments-hash)
264 | (gethash data-type company-terraform-data-attributes-hash))
265 | t))
266 | (`("module" ,module-name ,x)
267 | ;; Complete module output
268 | (company-terraform--filterdoc x
269 | (company-terraform-get-resource-cache
270 | 'output
271 | (gethash module-name (company-terraform-get-resource-cache 'module-dir)))
272 | ))
273 | (`(,resource-type ,x)
274 | ;; Complete resource name.
275 | (company-terraform--filterdoc x
276 | (gethash resource-type (company-terraform-get-resource-cache 'resource))))
277 | (`(,resource-type ,resource-name . ,(or `(,x)
278 | `(,(pred company-terraform-is-resource-n) ,x)))
279 | ;; Complete resource arguments/attributes
280 | (company-terraform--filterdoc x
281 | (list (gethash resource-type company-terraform-resource-arguments-hash)
282 | (gethash resource-type company-terraform-resource-attributes-hash))
283 | t)))))))
284 |
285 | (defun company-terraform-doc (candidate)
286 | "Return the documentation of a completion CANDIDATE."
287 | (get-text-property 0 'doc candidate))
288 |
289 | (defun company-terraform-docbuffer (candidate)
290 | "Prepare a temporary buffer with completion CANDIDATE documentation."
291 | (company-doc-buffer (company-terraform-doc candidate)))
292 |
293 | ;;;###autoload
294 | (defun company-terraform (command &optional arg &rest ignored)
295 | "Main entry point for a company backend.
296 | Read `company-mode` function docs for the semantics of this function."
297 | (cl-case command
298 | (interactive (company-begin-backend 'company-test-backend))
299 | (prefix (company-terraform--prefix))
300 | (candidates (company-terraform-candidates arg))
301 | (meta (company-terraform-doc arg))
302 | (doc-buffer (company-terraform-docbuffer arg))))
303 |
304 |
305 | ;;;###autoload
306 | (defun company-terraform-init ()
307 | "Add terraform to the company backends."
308 | (interactive)
309 | (add-to-list 'company-backends 'company-terraform))
310 |
311 | (provide 'company-terraform)
312 |
313 | ;;; company-terraform.el ends here
314 |
--------------------------------------------------------------------------------
|