]> git.immae.eu Git - github/fretlink/purs-loader.git/blob - src/ide.js
Add options for ide commands
[github/fretlink/purs-loader.git] / src / ide.js
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');
16
17 const debug = debug_('purs-loader');
18
19 const debugVerbose = debug_('purs-loader:verbose');
20
21 const dargs = require('./dargs');
22
23 const compile = require('./compile');
24
25 const PsModuleMap = require('./purs-module-map');
26
27 function UnknownModuleError() {
28 this.name = 'UnknownModuleError';
29 this.stack = (new Error()).stack;
30 }
31
32 UnknownModuleError.prototype = Object.create(Error.prototype);
33
34 UnknownModuleError.prototype.constructor = UnknownModuleError;
35
36 module.exports.UnknownModuleError = UnknownModuleError;
37
38 function spawnIdeClient(body, options) {
39 const ideClientCommand = options.pscIdeClient || 'purs';
40
41 const ideClientArgs = (options.pscIdeClient ? [] : ['ide', 'client']).concat(dargs(options.pscIdeClientArgs));
42
43 const stderr = [];
44
45 const stdout = [];
46
47 debug('ide client %s %o %O', ideClientCommand, ideClientArgs, body);
48
49 return new Promise((resolve, reject) => {
50 const ideClient = spawn(ideClientCommand, ideClientArgs);
51
52 ideClient.stderr.on('data', data => {
53 stderr.push(data.toString());
54 })
55
56 ideClient.stdout.on('data', data => {
57 stdout.push(data.toString());
58 })
59
60 ideClient.on('close', code => {
61 if (code !== 0) {
62 const errorMessage = stderr.join('');
63
64 reject(new Error(`ide client failed: ${errorMessage}`));
65 }
66 else {
67 const result = stdout.join('');
68
69 resolve(result);
70 }
71 })
72
73 ideClient.stdin.resume();
74
75 ideClient.stdin.write(JSON.stringify(body));
76
77 ideClient.stdin.write('\n');
78 });
79 }
80
81 function formatIdeResult(result, options, index, length) {
82 let numAndErr = `[${index+1}/${length} ${result.errorCode}]`
83 numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr
84
85 function makeResult() {
86 return Promise.resolve(`\n${numAndErr} ${result.message}`)
87 }
88
89 function makeResultSnippet(filename, pos) {
90 const srcPath = path.relative(options.context, filename);
91 const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}`
92
93 return fs.readFileAsync(filename, 'utf8').then(source => {
94 const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine)
95 const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine
96 const up = options.pscIdeColors ? colors.red('^') : '^'
97 const down = options.pscIdeColors ? colors.red('v') : 'v'
98 let trimmed = lines.slice(0)
99
100 if (endsOnNewline) {
101 lines.splice(lines.length - 1, 1)
102 pos.endLine = pos.endLine - 1
103 pos.endColumn = lines[lines.length - 1].length || 1
104 }
105
106 // strip newlines at the end
107 if (endsOnNewline) {
108 trimmed = lines.reverse().reduce((trimmed, line, i) => {
109 if (i === 0 && line === '') trimmed.trimming = true
110 if (!trimmed.trimming) trimmed.push(line)
111 if (trimmed.trimming && line !== '') {
112 trimmed.trimming = false
113 trimmed.push(line)
114 }
115 return trimmed
116 }, []).reverse()
117 pos.endLine = pos.endLine - (lines.length - trimmed.length)
118 pos.endColumn = trimmed[trimmed.length - 1].length || 1
119 }
120
121 const spaces = ' '.repeat(String(pos.endLine).length)
122 let snippet = trimmed.map((line, i) => {
123 return ` ${pos.startLine + i} ${line}`
124 }).join('\n')
125
126 if (trimmed.length === 1) {
127 snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
128 } else {
129 snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
130 snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
131 }
132
133 return Promise.resolve(`\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`)
134 }).catch(error => {
135 debug('failed to format ide result: %o', error);
136
137 return Promise.resolve('');
138 });
139 }
140
141 return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult();
142 }
143
144 module.exports.connect = function connect(psModule) {
145 const options = psModule.options
146
147 const serverCommand = options.pscIdeServer || 'purs';
148
149 const serverArgs = (options.pscIdeServer ? [] : ['ide', 'server']).concat(dargs(Object.assign({
150 outputDirectory: options.output,
151 '_': options.src
152 }, options.pscIdeServerArgs)));
153
154 debug('ide server: %s %o', serverCommand, serverArgs);
155
156 const ideServer = spawn(serverCommand, serverArgs);
157
158 ideServer.stdout.on('data', data => {
159 debugVerbose('ide server stdout: %s', data.toString());
160 });
161
162 ideServer.stderr.on('data', data => {
163 debugVerbose('ide server stderr: %s', data.toString());
164 });
165
166 ideServer.on('error', error => {
167 debugVerbose('ide server error: %o', error);
168 });
169
170 ideServer.on('close', (code, signal) => {
171 debugVerbose('ide server close: %s %s', code, signal);
172 });
173
174 return Promise.resolve(ideServer);
175 };
176
177 module.exports.load = function load(psModule) {
178 const options = psModule.options
179
180 const body = {command: 'load'};
181
182 return spawnIdeClient(body, options);
183 };
184
185 module.exports.loadWithRetry = function loadWithRetry(psModule) {
186 const retries = 9;
187
188 return retryPromise((retry, number) => {
189 debugVerbose('attempting to load modules (%d out of %d attempts)', number, retries);
190
191 return module.exports.load(psModule).catch(retry);
192 }, {
193 retries: retries,
194 factor: 1,
195 minTimeout: 333,
196 maxTimeout: 333,
197 }).then(() => psModule);
198 };
199
200 module.exports.rebuild = function rebuild(psModule) {
201 const options = psModule.options;
202
203 const body = {
204 command: 'rebuild',
205 params: {
206 file: psModule.srcPath,
207 }
208 };
209
210 const parseResponse = response => {
211 try {
212 const parsed = JSON.parse(response);
213
214 debugVerbose('parsed JSON response: %O', parsed);
215
216 return Promise.resolve(parsed);
217 }
218 catch (error) {
219 return Promise.reject(error);
220 }
221 };
222
223 const formatResponse = parsed => {
224 const result = Array.isArray(parsed.result) ? parsed.result : [];
225
226 return Promise.map(result, (item, i) => {
227 debugVerbose('formatting result %O', item);
228
229 return formatIdeResult(item, options, i, result.length);
230 }).then(formatted => ({
231 parsed: parsed,
232 formatted: formatted,
233 formattedMessage: formatted.join('\n')
234 }));
235 };
236
237 return spawnIdeClient(body, options)
238 .then(parseResponse)
239 .then(formatResponse)
240 .then(({ parsed, formatted, formattedMessage }) => {
241 if (parsed.resultType === 'success') {
242 if (options.warnings && formattedMessage.length) {
243 psModule.emitWarning(formattedMessage);
244 }
245
246 return psModule;
247 }
248 else if ((parsed.result || []).some(item => {
249 const isModuleNotFound = item.errorCode === 'ModuleNotFound';
250
251 const isUnknownModule = item.errorCode === 'UnknownModule';
252
253 const isUnknownModuleImport = item.errorCode === 'UnknownName' && /Unknown module/.test(item.message);
254
255 return isModuleNotFound || isUnknownModule || isUnknownModuleImport;
256 })) {
257 debug('module %s was not rebuilt because the module is unknown', psModule.name);
258
259 return Promise.reject(new UnknownModuleError());
260 }
261 else {
262 if (formattedMessage.length) {
263 psModule.emitError(formattedMessage);
264 }
265
266 return psModule;
267 }
268 })
269 ;
270 };