diff options
Diffstat (limited to 'src/PscIde.js')
-rw-r--r-- | src/PscIde.js | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/src/PscIde.js b/src/PscIde.js new file mode 100644 index 0000000..9d6c1ff --- /dev/null +++ b/src/PscIde.js | |||
@@ -0,0 +1,237 @@ | |||
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 | 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 connect to psc-ide-server', args) | ||
35 | |||
36 | const ideClient = spawn('psc-ide-client', args) | ||
37 | |||
38 | ideClient.stderr.on('data', data => { | ||
39 | debug(data.toString()) | ||
40 | cache.ideServer = false | ||
41 | reject(true) | ||
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(true) | ||
53 | } | ||
54 | } else { | ||
55 | cache.ideServer = false | ||
56 | reject(true) | ||
57 | } | ||
58 | }) | ||
59 | ideClient.stdin.resume() | ||
60 | ideClient.stdin.write(JSON.stringify({ command: 'load' })) | ||
61 | ideClient.stdin.write('\n') | ||
62 | }) | ||
63 | |||
64 | const args = dargs(Object.assign({ | ||
65 | outputDirectory: options.output, | ||
66 | }, options.pscIdeArgs)) | ||
67 | |||
68 | debug('attempting to start psc-ide-server', args) | ||
69 | |||
70 | const ideServer = cache.ideServer = spawn('psc-ide-server', []) | ||
71 | ideServer.stderr.on('data', data => { | ||
72 | debug(data.toString()) | ||
73 | }) | ||
74 | |||
75 | return retryPromise((retry, number) => { | ||
76 | return connect_().catch(error => { | ||
77 | if (!cache.ideServer && number === 9) { | ||
78 | debug(error) | ||
79 | |||
80 | console.log( | ||
81 | 'failed to connect to or start psc-ide-server, ' + | ||
82 | 'full compilation will occur on rebuild' | ||
83 | ) | ||
84 | |||
85 | return Promise.resolve(psModule) | ||
86 | } | ||
87 | |||
88 | return retry(error) | ||
89 | }) | ||
90 | }, { | ||
91 | retries: 9, | ||
92 | factor: 1, | ||
93 | minTimeout: 333, | ||
94 | maxTimeout: 333, | ||
95 | }) | ||
96 | } | ||
97 | module.exports.connect = connect; | ||
98 | |||
99 | function rebuild(psModule) { | ||
100 | const options = psModule.options | ||
101 | const cache = psModule.cache | ||
102 | |||
103 | debug('attempting rebuild with psc-ide-client %s', psModule.srcPath) | ||
104 | |||
105 | const request = (body) => new Promise((resolve, reject) => { | ||
106 | const args = dargs(options.pscIdeArgs) | ||
107 | const ideClient = spawn('psc-ide-client', args) | ||
108 | |||
109 | var stdout = '' | ||
110 | var stderr = '' | ||
111 | |||
112 | ideClient.stdout.on('data', data => { | ||
113 | stdout = stdout + data.toString() | ||
114 | }) | ||
115 | |||
116 | ideClient.stderr.on('data', data => { | ||
117 | stderr = stderr + data.toString() | ||
118 | }) | ||
119 | |||
120 | ideClient.on('close', code => { | ||
121 | if (code !== 0) { | ||
122 | const error = stderr === '' ? 'Failed to spawn psc-ide-client' : stderr | ||
123 | return reject(new Error(error)) | ||
124 | } | ||
125 | |||
126 | let res = null | ||
127 | |||
128 | try { | ||
129 | res = JSON.parse(stdout.toString()) | ||
130 | debug(res) | ||
131 | } catch (err) { | ||
132 | return reject(err) | ||
133 | } | ||
134 | |||
135 | if (res && !Array.isArray(res.result)) { | ||
136 | return res.resultType === 'success' | ||
137 | ? resolve(psModule) | ||
138 | : reject('psc-ide rebuild failed') | ||
139 | } | ||
140 | |||
141 | Promise.map(res.result, (item, i) => { | ||
142 | debug(item) | ||
143 | return formatIdeResult(item, options, i, res.result.length) | ||
144 | }) | ||
145 | .then(compileMessages => { | ||
146 | if (res.resultType === 'error') { | ||
147 | if (res.result.some(item => item.errorCode === 'UnknownModule' || item.errorCode === 'UnknownName')) { | ||
148 | debug('unknown module, attempting full recompile') | ||
149 | return Psc.compile(psModule) | ||
150 | .then(() => PsModuleMap.makeMap(options.src).then(map => { | ||
151 | debug('rebuilt module map'); | ||
152 | cache.psModuleMap = map; | ||
153 | })) | ||
154 | .then(() => request({ command: 'load' })) | ||
155 | .then(resolve) | ||
156 | .catch(() => reject('psc-ide rebuild failed')) | ||
157 | } | ||
158 | cache.errors = compileMessages.join('\n') | ||
159 | reject('psc-ide rebuild failed') | ||
160 | } else { | ||
161 | cache.warnings = compileMessages.join('\n') | ||
162 | resolve(psModule) | ||
163 | } | ||
164 | }) | ||
165 | }) | ||
166 | |||
167 | ideClient.stdin.write(JSON.stringify(body)) | ||
168 | ideClient.stdin.write('\n') | ||
169 | }) | ||
170 | |||
171 | return request({ | ||
172 | command: 'rebuild', | ||
173 | params: { | ||
174 | file: psModule.srcPath, | ||
175 | } | ||
176 | }) | ||
177 | } | ||
178 | module.exports.rebuild = rebuild; | ||
179 | |||
180 | function formatIdeResult(result, options, index, length) { | ||
181 | let numAndErr = `[${index+1}/${length} ${result.errorCode}]` | ||
182 | numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr | ||
183 | |||
184 | function makeResult() { | ||
185 | return Promise.resolve(`\n${numAndErr} ${result.message}`) | ||
186 | } | ||
187 | |||
188 | function makeResultSnippet(filename, pos) { | ||
189 | const srcPath = path.relative(options.context, filename); | ||
190 | const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}` | ||
191 | |||
192 | return fs.readFileAsync(filename, 'utf8').then(source => { | ||
193 | const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine) | ||
194 | const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine | ||
195 | const up = options.pscIdeColors ? colors.red('^') : '^' | ||
196 | const down = options.pscIdeColors ? colors.red('v') : 'v' | ||
197 | let trimmed = lines.slice(0) | ||
198 | |||
199 | if (endsOnNewline) { | ||
200 | lines.splice(lines.length - 1, 1) | ||
201 | pos.endLine = pos.endLine - 1 | ||
202 | pos.endColumn = lines[lines.length - 1].length || 1 | ||
203 | } | ||
204 | |||
205 | // strip newlines at the end | ||
206 | if (endsOnNewline) { | ||
207 | trimmed = lines.reverse().reduce((trimmed, line, i) => { | ||
208 | if (i === 0 && line === '') trimmed.trimming = true | ||
209 | if (!trimmed.trimming) trimmed.push(line) | ||
210 | if (trimmed.trimming && line !== '') { | ||
211 | trimmed.trimming = false | ||
212 | trimmed.push(line) | ||
213 | } | ||
214 | return trimmed | ||
215 | }, []).reverse() | ||
216 | pos.endLine = pos.endLine - (lines.length - trimmed.length) | ||
217 | pos.endColumn = trimmed[trimmed.length - 1].length || 1 | ||
218 | } | ||
219 | |||
220 | const spaces = ' '.repeat(String(pos.endLine).length) | ||
221 | let snippet = trimmed.map((line, i) => { | ||
222 | return ` ${pos.startLine + i} ${line}` | ||
223 | }).join('\n') | ||
224 | |||
225 | if (trimmed.length === 1) { | ||
226 | snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}` | ||
227 | } else { | ||
228 | snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}` | ||
229 | snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}` | ||
230 | } | ||
231 | |||
232 | return Promise.resolve(`\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`) | ||
233 | }) | ||
234 | } | ||
235 | |||
236 | return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult(); | ||
237 | } | ||