diff options
Diffstat (limited to 'src/ide.js')
-rw-r--r-- | src/ide.js | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/src/ide.js b/src/ide.js new file mode 100644 index 0000000..f839fd5 --- /dev/null +++ b/src/ide.js | |||
@@ -0,0 +1,260 @@ | |||
1 | 'use strict'; | ||
2 | |||
3 | const path = require('path'); | ||
4 | |||
5 | const Promise = require('bluebird'); | ||
6 | |||
7 | const fs = Promise.promisifyAll(require('fs')); | ||
8 | |||
9 | const retryPromise = require('promise-retry'); | ||
10 | |||
11 | const spawn = require('cross-spawn'); | ||
12 | |||
13 | const colors = require('chalk'); | ||
14 | |||
15 | const debug = require('debug')('purs-loader'); | ||
16 | |||
17 | const dargs = require('./dargs'); | ||
18 | |||
19 | const Psc = require('./Psc'); | ||
20 | |||
21 | const PsModuleMap = require('./PsModuleMap'); | ||
22 | |||
23 | module.exports.connect = function connect(psModule) { | ||
24 | const options = psModule.options | ||
25 | const cache = psModule.cache | ||
26 | |||
27 | if (cache.ideServer) return Promise.resolve(psModule) | ||
28 | |||
29 | cache.ideServer = true | ||
30 | |||
31 | const connect_ = () => new Promise((resolve, reject) => { | ||
32 | const args = dargs(options.pscIdeArgs) | ||
33 | |||
34 | debug('attempting to run purs ide client: %o', args) | ||
35 | |||
36 | const ideClient = spawn('purs', ['ide', 'client'].concat(args)) | ||
37 | |||
38 | ideClient.stderr.on('data', data => { | ||
39 | debug(data.toString()) | ||
40 | cache.ideServer = false | ||
41 | reject(new Error('purs ide client failed')) | ||
42 | }) | ||
43 | ideClient.stdout.once('data', data => { | ||
44 | debug(data.toString()) | ||
45 | if (data.toString()[0] === '{') { | ||
46 | const res = JSON.parse(data.toString()) | ||
47 | if (res.resultType === 'success') { | ||
48 | cache.ideServer = ideServer | ||
49 | resolve(psModule) | ||
50 | } else { | ||
51 | cache.ideServer = ideServer | ||
52 | reject(new Error('purs ide client failed')) | ||
53 | } | ||
54 | } else { | ||
55 | cache.ideServer = false | ||
56 | reject(new Error('purs ide client failed')) | ||
57 | } | ||
58 | }) | ||
59 | ideClient.stdin.resume() | ||
60 | ideClient.stdin.write(JSON.stringify({ command: 'load' })) | ||
61 | ideClient.stdin.write('\n') | ||
62 | }) | ||
63 | |||
64 | const serverArgs = dargs(Object.assign({ | ||
65 | outputDirectory: options.output, | ||
66 | '_': options.src | ||
67 | }, options.pscIdeServerArgs)) | ||
68 | |||
69 | debug('attempting to start purs ide server: %o', serverArgs) | ||
70 | |||
71 | const ideServer = cache.ideServer = spawn('purs', ['ide', 'server'].concat(serverArgs)) | ||
72 | |||
73 | ideServer.stdout.on('data', data => { | ||
74 | debug('purs ide server stdout: %s', data.toString()); | ||
75 | }); | ||
76 | |||
77 | ideServer.stderr.on('data', data => { | ||
78 | debug('purs ide server stderr: %s', data.toString()); | ||
79 | }); | ||
80 | |||
81 | ideServer.on('error', error => { | ||
82 | debug('purs ide server error: %o', error); | ||
83 | }); | ||
84 | |||
85 | ideServer.on('close', (code, signal) => { | ||
86 | debug('purs ide server close: %s %s', code, signal); | ||
87 | }); | ||
88 | |||
89 | return retryPromise((retry, number) => { | ||
90 | return connect_().catch(error => { | ||
91 | if (!cache.ideServer && number === 9) { | ||
92 | debug(error) | ||
93 | |||
94 | console.warn('Failed to connect to or start purs ide server. A full compilation will occur on rebuild'); | ||
95 | |||
96 | return Promise.resolve(psModule) | ||
97 | } | ||
98 | |||
99 | return retry(error) | ||
100 | }) | ||
101 | }, { | ||
102 | retries: 9, | ||
103 | factor: 1, | ||
104 | minTimeout: 333, | ||
105 | maxTimeout: 333, | ||
106 | }) | ||
107 | }; | ||
108 | |||
109 | module.exports.rebuild = function rebuild(psModule) { | ||
110 | const options = psModule.options | ||
111 | const cache = psModule.cache | ||
112 | |||
113 | debug('attempting rebuild with purs ide client %s', psModule.srcPath) | ||
114 | |||
115 | const request = (body) => new Promise((resolve, reject) => { | ||
116 | const args = dargs(options.pscIdeArgs) | ||
117 | const ideClient = spawn('purs', ['ide', 'client'].concat(args)) | ||
118 | |||
119 | var stdout = '' | ||
120 | var stderr = '' | ||
121 | |||
122 | ideClient.stdout.on('data', data => { | ||
123 | stdout = stdout + data.toString() | ||
124 | }) | ||
125 | |||
126 | ideClient.stderr.on('data', data => { | ||
127 | stderr = stderr + data.toString() | ||
128 | }) | ||
129 | |||
130 | ideClient.on('close', code => { | ||
131 | if (code !== 0) { | ||
132 | const error = stderr === '' ? 'Failed to spawn purs ide client' : stderr | ||
133 | return reject(new Error(error)) | ||
134 | } | ||
135 | |||
136 | let res = null | ||
137 | |||
138 | try { | ||
139 | res = JSON.parse(stdout.toString()) | ||
140 | debug(res) | ||
141 | } catch (err) { | ||
142 | return reject(err) | ||
143 | } | ||
144 | |||
145 | if (res && !Array.isArray(res.result)) { | ||
146 | return resolve(psModule); | ||
147 | } | ||
148 | |||
149 | Promise.map(res.result, (item, i) => { | ||
150 | debug(item) | ||
151 | return formatIdeResult(item, options, i, res.result.length) | ||
152 | }) | ||
153 | .then(compileMessages => { | ||
154 | if (res.resultType === 'error') { | ||
155 | if (res.result.some(item => { | ||
156 | const isModuleNotFound = item.errorCode === 'ModuleNotFound'; | ||
157 | |||
158 | const isUnknownModule = item.errorCode === 'UnknownModule'; | ||
159 | |||
160 | const isUnknownModuleImport = item.errorCode === 'UnknownName' && /Unknown module/.test(item.message); | ||
161 | |||
162 | return isModuleNotFound || isUnknownModule || isUnknownModuleImport; | ||
163 | })) { | ||
164 | debug('unknown module, attempting full recompile') | ||
165 | return Psc.compile(psModule) | ||
166 | .then(() => PsModuleMap.makeMap(options.src).then(map => { | ||
167 | debug('rebuilt module map after unknown module forced a recompile'); | ||
168 | cache.psModuleMap = map; | ||
169 | })) | ||
170 | .then(() => request({ command: 'load' })) | ||
171 | .then(resolve) | ||
172 | .catch(() => resolve(psModule)) | ||
173 | } | ||
174 | const errorMessage = compileMessages.join('\n'); | ||
175 | if (errorMessage.length) { | ||
176 | psModule.emitError(errorMessage); | ||
177 | } | ||
178 | resolve(psModule); | ||
179 | } else { | ||
180 | const warningMessage = compileMessages.join('\n'); | ||
181 | if (options.warnings && warningMessage.length) { | ||
182 | psModule.emitWarning(warningMessage); | ||
183 | } | ||
184 | resolve(psModule); | ||
185 | } | ||
186 | }) | ||
187 | }) | ||
188 | |||
189 | debug('purs ide client stdin: %o', body); | ||
190 | |||
191 | ideClient.stdin.write(JSON.stringify(body)) | ||
192 | ideClient.stdin.write('\n') | ||
193 | }) | ||
194 | |||
195 | return request({ | ||
196 | command: 'rebuild', | ||
197 | params: { | ||
198 | file: psModule.srcPath, | ||
199 | } | ||
200 | }) | ||
201 | }; | ||
202 | |||
203 | function formatIdeResult(result, options, index, length) { | ||
204 | let numAndErr = `[${index+1}/${length} ${result.errorCode}]` | ||
205 | numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr | ||
206 | |||
207 | function makeResult() { | ||
208 | return Promise.resolve(`\n${numAndErr} ${result.message}`) | ||
209 | } | ||
210 | |||
211 | function makeResultSnippet(filename, pos) { | ||
212 | const srcPath = path.relative(options.context, filename); | ||
213 | const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}` | ||
214 | |||
215 | return fs.readFileAsync(filename, 'utf8').then(source => { | ||
216 | const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine) | ||
217 | const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine | ||
218 | const up = options.pscIdeColors ? colors.red('^') : '^' | ||
219 | const down = options.pscIdeColors ? colors.red('v') : 'v' | ||
220 | let trimmed = lines.slice(0) | ||
221 | |||
222 | if (endsOnNewline) { | ||
223 | lines.splice(lines.length - 1, 1) | ||
224 | pos.endLine = pos.endLine - 1 | ||
225 | pos.endColumn = lines[lines.length - 1].length || 1 | ||
226 | } | ||
227 | |||
228 | // strip newlines at the end | ||
229 | if (endsOnNewline) { | ||
230 | trimmed = lines.reverse().reduce((trimmed, line, i) => { | ||
231 | if (i === 0 && line === '') trimmed.trimming = true | ||
232 | if (!trimmed.trimming) trimmed.push(line) | ||
233 | if (trimmed.trimming && line !== '') { | ||
234 | trimmed.trimming = false | ||
235 | trimmed.push(line) | ||
236 | } | ||
237 | return trimmed | ||
238 | }, []).reverse() | ||
239 | pos.endLine = pos.endLine - (lines.length - trimmed.length) | ||
240 | pos.endColumn = trimmed[trimmed.length - 1].length || 1 | ||
241 | } | ||
242 | |||
243 | const spaces = ' '.repeat(String(pos.endLine).length) | ||
244 | let snippet = trimmed.map((line, i) => { | ||
245 | return ` ${pos.startLine + i} ${line}` | ||
246 | }).join('\n') | ||
247 | |||
248 | if (trimmed.length === 1) { | ||
249 | snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}` | ||
250 | } else { | ||
251 | snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}` | ||
252 | snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}` | ||
253 | } | ||
254 | |||
255 | return Promise.resolve(`\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`) | ||
256 | }) | ||
257 | } | ||
258 | |||
259 | return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult(); | ||
260 | } | ||