aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/textadept/run.lua
blob: 7f5175292fee336130ff3e31db5df4e4119fd114 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
-- Copyright 2007-2022 Mitchell. See LICENSE.

local M = {}

--[[ This comment is for LuaDoc.
---
-- Compile and run source code files with Textadept.
-- [Language modules](#compile-and-run) may tweak the `compile_commands`, `run_commands`, and
-- `error_patterns` tables for particular languages.
-- The user may tweak `build_commands` and `test_commands` for particular projects.
-- @field run_in_background (bool)
--   Run shell commands silently in the background.
--   This only applies when the message buffer is open, though it does not have to be visible.
--   The default value is `false`.
-- @field MARK_WARNING (number)
--   The run or compile warning marker number.
-- @field MARK_ERROR (number)
--   The run or compile error marker number.
-- @field _G.events.COMPILE_OUTPUT (string)
--   Emitted when executing a language's compile shell command.
--   By default, compiler output is printed to the message buffer. In order to override this
--   behavior, connect to the event with an index of `1` and return `true`.
--   Arguments:
--
--   * `output`: A line of string output from the command.
--   * `ext_or_lexer`: The file extension or lexer name associated with the executed compile
--     command.
-- @field _G.events.RUN_OUTPUT (string)
--   Emitted when executing a language's run shell command.
--   By default, output is printed to the message buffer. In order to override this behavior,
--   connect to the event with an index of `1` and return `true`.
--   Arguments:
--
--   * `output`: A line of string output from the command.
--   * `ext_or_lexer`: The file extension or lexer name associated with the executed run command.
-- @field _G.events.BUILD_OUTPUT (string)
--   Emitted when executing a project's build shell command.
--   By default, output is printed to the message buffer. In order to override this behavior,
--   connect to the event with an index of `1` and return `true`.
--   Arguments:
--
--   * `output`: A line of string output from the command.
-- @field _G.events.TEST_OUTPUT (string)
--   Emitted when executing a project's shell command for running tests.
--   By default, output is printed to the message buffer. In order to override this behavior,
--   connect to the event with an index of `1` and return `true`.
--   Arguments:
--
--   * `output`: A line of string output from the command.
module('textadept.run')]]

M.run_in_background = false

M.MARK_WARNING = _SCINTILLA.next_marker_number()
M.MARK_ERROR = _SCINTILLA.next_marker_number()

-- Events.
local run_events = {'compile_output', 'run_output', 'build_output', 'test_output'}
for _, v in ipairs(run_events) do events[v:upper()] = v end

-- Keep track of: the last process spawned in order to kill it if requested; the cwd of that
-- process in order to jump to relative file paths in recognized warning or error messages;
-- and the view the process was spawned from in order to jump to messages (which are displayed
-- in a split view) in the original view.
local proc, cwd, preferred_view

-- Scans the given message for a warning or error message and, if one is found, returns table
-- of the warning/error's details.
-- @param message The message to parse for warnings or errors. The message is assumed to be
--   encoded in _CHARSET.
-- @param ext_or_lexer Optional file extension or lexer name associated with the shell command
--   that produced the warning/error.
-- @return error details table with 'filename', 'line', 'column', and 'message' fields along
--   with a 'warning' flag.
-- @see error_patterns
local function scan_for_error(message, ext_or_lexer)
  for key, patterns in pairs(M.error_patterns) do
    if ext_or_lexer and key ~= ext_or_lexer then goto continue end
    for _, patt in ipairs(patterns) do
      if not message:find(patt) then goto continue end
      -- Extract details from the warning or error.
      local detail, i = {message:match(patt)}, 1
      for capture in patt:gmatch('[^%%](%b())') do
        if capture == '(.-)' then
          detail.filename = detail[i]
        elseif capture == '(%d+)' then
          local line_or_column = not detail.line and 'line' or 'column'
          detail[line_or_column] = tonumber(detail[i])
        else
          detail.message = detail[i]
        end
        i = i + 1
      end
      local lower_message = message:lower()
      detail.warning = (lower_message:find('warning') or lower_message:find('note')) and
        not lower_message:find('error')
      -- Compile and run commands specify the file extension or lexer name used to determine
      -- the command, so the error patterns used are guaranteed to be correct. Build and
      -- test commands have no such context and instead iterate through all possible error
      -- patterns. Only consider the error/warning valid if the extracted filename's extension
      -- or lexer name matches the error pattern's extension or lexer name.
      if ext_or_lexer then return detail end
      local ext = detail.filename:match('[^/\\.]+$')
      local lexer_name = textadept.file_types.extensions[ext]
      if key == ext or key == lexer_name then return detail end
      ::continue::
    end
    ::continue::
  end
  return nil
end

-- Prints an output line from a compile, run, build, or test shell command.
-- Assume output is UTF-8 unless there's a recognized warning or error message. In that case
-- assume it is encoded in _CHARSET and mark it.
-- All stdout and stderr from the command is printed silently.
-- @param line The output line to print.
-- @param ext_or_lexer Optional file extension or lexer name associated with the executed command.
--   This is used for better error detection in compile and run commands.
local function print_line(line, ext_or_lexer)
  local error = scan_for_error(line, ext_or_lexer)
  ui.silent_print = M.run_in_background or ext_or_lexer or not line:find('^> ') or
    line:find('^> exit')
  ui.print(not error and line or line:iconv('UTF-8', _CHARSET))
  ui.silent_print = false
  if error then
    -- Current position is one line below the error due to ui.print()'s '\n'.
    buffer:marker_add(buffer.line_count - 1, error.warning and M.MARK_WARNING or M.MARK_ERROR)
  end
end

local output_buffer
-- Prints the output from a compile, run, build, or test shell command as a series of lines,
-- performing buffering as needed.
-- @param output The output to print, or `nil` to flush any buffered output.
-- @param ext_or_lexer Optional file extension or lexer name associated with the executed command.
--   This is used for better error detection in compile and run commands.
local function print_output(output, ext_or_lexer)
  if output then
    if output_buffer then output = output_buffer .. output end
    local remainder = 1
    for line, e in output:gmatch('([^\r\n]*)\r?\n()') do
      print_line(line, ext_or_lexer)
      remainder = e
    end
    output_buffer = remainder <= #output and output:sub(remainder)
  elseif output_buffer then
    print_line(output_buffer, ext_or_lexer)
    output_buffer = nil
  end
end

-- Runs command *command* in working directory *dir*, emitting events of type *event* with any
-- output received.
-- @param command String command to run, or a function returning such a string and optional
--   working directory and environment table. A returned working directory overrides *dir*.
-- @param dir String working directory to run *command* in.
-- @param event String event name to emit command output with.
-- @param macros Optional table of '%[char]' macros to expand within *command*.
-- @param ext_or_lexer Optional file extension or lexer name associated with the executed command.
--   This is used for better error detection in compile and run commands.
local function run_command(command, dir, event, macros, ext_or_lexer)
  local working_dir, env
  if type(command) == 'function' then command, working_dir, env = command() end
  if not command then return end
  if macros then command = command:gsub('%%%a', macros) end
  preferred_view = view
  local function emit(output) events.emit(event, output, ext_or_lexer) end
  cwd = (working_dir or dir):gsub('[/\\]$', '')
  events.emit(event, string.format('> cd %s\n', cwd))
  events.emit(event, string.format('> %s\n', command:iconv('UTF-8', _CHARSET)))
  local args = {
    command, cwd, emit, emit, function(status)
      emit() -- flush
      events.emit(event, string.format('> exit status: %d\n', status))
    end
  }
  if env then table.insert(args, 3, env) end
  proc = assert(os.spawn(table.unpack(args)))
end

-- Compiles or runs file *filename* based on a shell command in *commands*.
-- @param filename The file to run.
-- @param commands Either `compile_commands` or `run_commands`.
local function compile_or_run(filename, commands)
  if filename == buffer.filename then
    buffer:annotation_clear_all()
    if buffer.modify then buffer:save() end
  end
  local ext = filename:match('[^/\\.]+$')
  local lang = filename == buffer.filename and buffer:get_lexer() or
    textadept.file_types.extensions[ext]
  local command = commands[filename] or commands[ext] or commands[lang]
  local dirname, basename = '', filename
  if filename:find('[/\\]') then dirname, basename = filename:match('^(.+)[/\\]([^/\\]+)$') end
  local event = commands == M.compile_commands and events.COMPILE_OUTPUT or events.RUN_OUTPUT
  local macros = {
    ['%p'] = filename, ['%d'] = dirname, ['%f'] = basename, ['%e'] = basename:match('^(.+)%.') -- no extension
  }
  run_command(command, dirname, event, macros, commands[ext] and ext or lang)
end

-- LuaFormatter off
---
-- Map of filenames, file extensions, and lexer names to their associated "compile" shell
-- command line strings or functions that return such strings.
-- Command line strings may have the following macros:
--
--   + `%f`: The file's name, including its extension.
--   + `%e`: The file's name, excluding its extension.
--   + `%d`: The file's directory path.
--   + `%p`: The file's full path.
--
-- Functions may also return a working directory and process environment table to operate in. By
-- default, the working directory is the current file's parent directory and the environment
-- is Textadept's environment.
-- @class table
-- @name compile_commands
M.compile_commands = {actionscript='mxmlc "%f"',ada='gnatmake "%f"',ansi_c='gcc -o "%e" "%f"',antlr='antlr4 "%f"',g='antlr3 "%f"',applescript='osacompile "%f" -o "%e.scpt"',asm='nasm "%f"'--[[ && ld "%e.o" -o "%e"']],boo='booc "%f"',caml='ocamlc -o "%e" "%f"',csharp=WIN32 and 'csc "%f"' or 'mcs "%f"',coffeescript='coffee -c "%f"',context='context --nonstopmode "%f"',cpp='g++ -o "%e" "%f"',cuda=WIN32 and 'nvcc -o "%e.exe" "%f"' or 'nvcc -o "%e" "%f"',dmd='dmd "%f"',dot='dot -Tps "%f" -o "%e.ps"',eiffel='se c "%f"',elixir='elixirc "%f"',erlang='erl -compile "%e"',faust='faust -o "%e.cpp" "%f"',fsharp=WIN32 and 'fsc.exe "%f"' or 'mono fsc.exe "%f"',fortran='gfortran -o "%e" "%f"',gap='gac -o "%e" "%f"',go='go build "%f"',groovy='groovyc "%f"',haskell=WIN32 and 'ghc -o "%e.exe" "%f"' or 'ghc -o "%e" "%f"',inform=function() return 'inform -c "'..buffer.filename:match('^(.+%.inform[/\\])Source')..'"' end,java='javac "%f"',ltx='pdflatex -file-line-error -halt-on-error "%f"',less='lessc --no-color "%f" "%e.css"',lilypond='lilypond "%f"',lisp='clisp -c "%f"',litcoffee='coffee -c "%f"',lua='luac -o "%e.luac" "%f"',moon='moonc "%f"',markdown='markdown "%f" > "%e.html"',myr='mbld -b "%e" "%f"',nemerle='ncc "%f" -out:"%e.exe"',nim='nim c "%f"',nsis='MakeNSIS "%f"',objective_c='gcc -o "%e" "%f"',pascal='fpc "%f"',perl='perl -c "%f"',php='php -l "%f"',pony='ponyc "%f"',prolog='gplc --no-top-level "%f"',python='python -m py_compile "%f"',ruby='ruby -c "%f"',rust='rustc "%f"',sass='sass "%f" "%e.css"',scala='scalac "%f"',sml='mlton "%f"',tex='pdflatex -file-line-error -halt-on-error "%f"',typescript='tsc "%f"',vala='valac "%f"',vb=WIN32 and 'vbc "%f"' or 'vbnc "%f"',zig='zig build-exe "%f"'}
-- LuaFormatter on

---
-- Compiles file *filename* or the current file using an appropriate shell command from the
-- `compile_commands` table.
-- The shell command is determined from the file's filename, extension, or language in that order.
-- Emits `COMPILE_OUTPUT` events.
-- @param filename Optional path to the file to compile. The default value is the current
--   file's filename.
-- @see compile_commands
-- @see _G.events
-- @name compile
function M.compile(filename)
  if assert_type(filename, 'string/nil', 1) or buffer.filename then
    compile_or_run(filename or buffer.filename, M.compile_commands)
  end
end
events.connect(events.COMPILE_OUTPUT, print_output)

-- LuaFormatter off
---
-- Map of filenames, file extensions, and lexer names to their associated "run" shell command
-- line strings or functions that return strings.
-- Command line strings may have the following macros:
--
--   + `%f`: The file's name, including its extension.
--   + `%e`: The file's name, excluding its extension.
--   + `%d`: The file's directory path.
--   + `%p`: The file's full path.
--
-- Functions may also return a working directory and process environment table to operate in. By
-- default, the working directory is the current file's parent directory and the environment
-- is Textadept's environment.
-- @class table
-- @name run_commands
M.run_commands = {actionscript=WIN32 and 'start "" "%e.swf"' or OSX and 'open "file://%e.swf"' or 'xdg-open "%e.swf"',ada=WIN32 and '"%e"' or './"%e"',ansi_c=WIN32 and '"%e"' or './"%e"',applescript='osascript "%f"',asm='./"%e"',awk='awk -f "%f"',batch='"%f"',boo='booi "%f"',caml='ocamlrun "%e"',csharp=WIN32 and '"%e"' or 'mono "%e.exe"',chuck='chuck "%f"',clojure='clj -M "%f"',cmake='cmake -P "%f"',coffeescript='coffee "%f"',context=WIN32 and 'start "" "%e.pdf"' or OSX and 'open "%e.pdf"' or 'xdg-open "%e.pdf"',cpp=WIN32 and '"%e"' or './"%e"',crystal='crystal "%f"',cuda=WIN32 and '"%e"' or './"%e"',dart='dart "%f"',dmd=WIN32 and '"%e"' or './"%e"',eiffel="./a.out",elixir='elixir "%f"',fsharp=WIN32 and '"%e"' or 'mono "%e.exe"',fantom='fan "%f"',fennel='fennel "%f"',forth='gforth "%f" -e bye',fortran=WIN32 and '"%e"' or './"%e"',gnuplot='gnuplot "%f"',go='go run "%f"',groovy='groovy "%f"',haskell=WIN32 and '"%e"' or './"%e"',html=WIN32 and 'start "" "%f"' or OSX and 'open "file://%f"' or 'xdg-open "%f"',icon='icont "%e" -x',idl='idl -batch "%f"',Io='io "%f"',java='java "%e"',javascript='node "%f"',jq='jq -f "%f"',julia='julia "%f"',ltx=WIN32 and 'start "" "%e.pdf"' or OSX and 'open "%e.pdf"' or 'xdg-open "%e.pdf"',less='lessc --no-color "%f"',lilypond=WIN32 and 'start "" "%e.pdf"' or OSX and 'open "%e.pdf"' or 'xdg-open "%e.pdf"',lisp='clisp "%f"',litcoffee='coffee "%f"',lua='lua -e "io.stdout:setvbuf(\'no\')" "%f"',makefile=WIN32 and 'nmake -f "%f"' or 'make -f "%f"',markdown='markdown "%f"',moon='moon "%f"',myr=WIN32 and '"%e"' or './"%e"',nemerle=WIN32 and '"%e"' or 'mono "%e.exe"',nim='nim c -r "%f"',objective_c=WIN32 and '"%e"' or './"%e"',pascal=WIN32 and '"%e"' or './"%e"',perl='perl "%f"',php='php "%f"',pike='pike "%f"',pkgbuild='makepkg -p "%f"',pony=WIN32 and '"%e"' or './"%e"',prolog=WIN32 and '"%e"' or './"%e"',pure='pure "%f"',python=function() return buffer:get_line(1):find('^#!.-python3') and 'python3 -u "%f"' or 'python -u "%f"' end,rstats=WIN32 and 'Rterm -f "%f"' or 'R -f "%f"',rebol='REBOL "%f"',rexx=WIN32 and 'rexx "%f"' or 'regina "%f"',ruby='ruby "%f"',rust=WIN32 and '"%e"' or './"%e"',sass='sass "%f"',scala='scala "%e"',bash='bash "%f"',csh='tcsh "%f"',ksh='ksh "%f"',mksh='mksh "%f"',sh='sh "%f"',zsh='zsh "%f"',rc='rc "%f"',smalltalk='gst "%f"',sml=WIN32 and '"%e"' or './"%e"',snobol4='snobol4 -b "%f"',tcl='tclsh "%f"',tex=WIN32 and 'start "" "%e.pdf"' or OSX and 'open "%e.pdf"' or 'xdg-open "%e.pdf"',vala=WIN32 and '"%e"' or './"%e"',vb=WIN32 and '"%e"' or 'mono "%e.exe"',xs='xs "%f"',zig=WIN32 and '"%e"' or './"%e"'}
-- LuaFormatter on

---
-- Runs file *filename* or the current file using an appropriate shell command from the
-- `run_commands` table.
-- The shell command is determined from the file's filename, extension, or language in that order.
-- Emits `RUN_OUTPUT` events.
-- @param filename Optional path to the file to run. The default value is the current file's
--   filename.
-- @see run_commands
-- @see _G.events
-- @name run
function M.run(filename)
  if assert_type(filename, 'string/nil', 1) or buffer.filename then
    compile_or_run(filename or buffer.filename, M.run_commands)
  end
end
events.connect(events.RUN_OUTPUT, print_output)

---
-- Appends the command line argument strings *run* and *compile* to their respective run and
-- compile commands for file *filename* or the current file.
-- If either is `nil`, prompts the user for missing the arguments. Each filename has its own
-- set of compile and run arguments.
-- @param filename Optional path to the file to set run/compile arguments for.
-- @param run Optional string run arguments to set. If `nil`, the user is prompted for them. Pass
--   the empty string for no run arguments.
-- @param compile Optional string compile arguments to set. If `nil`, the user is prompted
--   for them. Pass the empty string for no compile arguments.
-- @see run_commands
-- @see compile_commands
-- @name set_arguments
function M.set_arguments(filename, run, compile)
  if not assert_type(filename, 'string/nil', 1) then
    filename = buffer.filename
    if not filename then return end
  end
  assert_type(run, 'string/nil', 2)
  assert_type(compile, 'string/nil', 3)
  local base_commands, utf8_args = {}, {}
  for i, commands in ipairs{M.run_commands, M.compile_commands} do
    -- Compare the base run/compile command with the one for the current file. The difference
    -- is any additional arguments set previously.
    base_commands[i] = commands[filename:match('[^.]+$')] or commands[buffer:get_lexer()] or ''
    local current_command = commands[filename] or ''
    local args = (i == 1 and run or compile) or current_command:sub(#base_commands[i] + 2)
    utf8_args[i] = args:iconv('UTF-8', _CHARSET)
  end
  if not run or not compile then
    local button
    button, utf8_args = ui.dialogs.inputbox{
      title = _L['Set Arguments...']:gsub('_', ''),
      informative_text = {_L['Command line arguments'], _L['For Run:'], _L['For Compile:']},
      text = utf8_args, width = not CURSES and 400 or nil
    }
    if button ~= 1 then return end
  end
  for i, commands in ipairs{M.run_commands, M.compile_commands} do
    -- Add the additional arguments to the base run/compile command and set the new command to
    -- be the one used for the current file.
    commands[filename] = string.format('%s %s', base_commands[i],
      utf8_args[i]:iconv(_CHARSET, 'UTF-8'))
  end
end

-- LuaFormatter off
---
-- Map of project root paths and "makefiles" to their associated "build" shell command line
-- strings or functions that return such strings.
-- Functions may also return a working directory and process environment table to operate
-- in. By default, the working directory is the project's root directory and the environment
-- is Textadept's environment.
-- @class table
-- @name build_commands
M.build_commands = {--[[Ant]]['build.xml']='ant',--[[Dockerfile]]Dockerfile='docker build .',--[[Make]]Makefile='make',GNUmakefile='make',makefile='make',--[[Meson]]['meson.build']='meson compile',--[[Maven]]['pom.xml']='mvn',--[[Ruby]]Rakefile='rake'}
-- LuaFormatter on

---
-- Builds the project whose root path is *root_directory* or the current project using the
-- shell command from the `build_commands` table.
-- If a "makefile" type of build file is found, prompts the user for the full build command. The
-- current project is determined by either the buffer's filename or the current working directory.
-- Emits `BUILD_OUTPUT` events.
-- @param root_directory The path to the project to build. The default value is the current project.
-- @see build_commands
-- @see _G.events
-- @name build
function M.build(root_directory)
  if not assert_type(root_directory, 'string/nil', 1) then
    root_directory = io.get_project_root()
    if not root_directory then return end
  end
  for i = 1, #_BUFFERS do _BUFFERS[i]:annotation_clear_all() end
  local command = M.build_commands[root_directory]
  if not command then
    for build_file, build_command in pairs(M.build_commands) do
      if lfs.attributes(string.format('%s/%s', root_directory, build_file)) then
        local button, utf8_command = ui.dialogs.inputbox{
          title = _L['Command'], informative_text = root_directory, text = build_command,
          button1 = _L['OK'], button2 = _L['Cancel']
        }
        if button == 1 then command = utf8_command:iconv(_CHARSET, 'UTF-8') end
        break
      end
    end
  end
  run_command(command, root_directory, events.BUILD_OUTPUT)
end
events.connect(events.BUILD_OUTPUT, print_output)

---
-- Map of project root paths to their associated "test" shell command line strings or functions
-- that return such strings.
-- Functions may also return a working directory and process environment table to operate
-- in. By default, the working directory is the project's root directory and the environment
-- is Textadept's environment.
-- @class table
-- @name test_commands
M.test_commands = {}

---
-- Runs tests for the project whose root path is *root_directory* or the current project using
-- the shell command from the `test_commands` table.
-- The current project is determined by either the buffer's filename or the current working
-- directory.
-- Emits `TEST_OUTPUT` events.
-- @param root_directory The path to the project to run tests for. The default value is the
--   current project.
-- @see test_commands
-- @see _G.events
-- @name test
function M.test(root_directory)
  if not assert_type(root_directory, 'string/nil', 1) then
    root_directory = io.get_project_root()
    if not root_directory then return end
  end
  for i = 1, #_BUFFERS do _BUFFERS[i]:annotation_clear_all() end
  run_command(M.test_commands[root_directory], root_directory, events.TEST_OUTPUT)
end
events.connect(events.TEST_OUTPUT, print_output)

---
-- Stops the currently running process, if any.
-- @name stop
function M.stop() if proc then proc:kill() end end

-- Returns whether or not the given buffer is the message buffer.
local function is_msg_buf(buf) return buf._type == _L['[Message Buffer]'] end

-- Send line as input to process stdin on return.
events.connect(events.CHAR_ADDED, function(code)
  if code == string.byte('\n') and proc and proc:status() == 'running' and is_msg_buf(buffer) then
    local line_num = buffer:line_from_position(buffer.current_pos) - 1
    proc:write(buffer:get_line(line_num))
  end
end)

-- LuaFormatter off
---
-- Map of file extensions and lexer names to their associated lists of string patterns that
-- match warning and error messages emitted by compile and run commands for those file extensions
-- and lexers.
-- Patterns match single lines and contain captures for a filename, line number, column number
-- (optional), and warning or error message (optional). Double-clicking a warning or error
-- message takes the user to the source of that warning/error.
-- Note: `(.-)` captures in patterns are interpreted as filenames; `(%d+)` captures are
-- interpreted as line numbers first, and then column numbers; and any other capture is treated
-- as warning/error message text.
-- @class table
-- @name error_patterns
M.error_patterns = {actionscript={'^(.-)%((%d+)%): col: (%d+) (.+)$'},ada={'^(.-):(%d+):(%d+):%s*(.*)$','^[^:]+: (.-):(%d+) (.+)$'},ansi_c={'^(.-):(%d+):(%d+): (.+)$'},antlr={'^error%(%d+%): (.-):(%d+):(%d+): (.+)$','^warning%(%d+%): (.-):(%d+):(%d+): (.+)$'},--[[ANTLR]]g={'^error%(%d+%): (.-):(%d+):(%d+): (.+)$','^warning%(%d+%): (.-):(%d+):(%d+): (.+)$'},asm={'^(.-):(%d+): (.+)$'},awk={'^awk: (.-):(%d+): (.+)$'},boo={'^(.-)%((%d+),(%d+)%): (.+)$'},caml={'^%s*File "(.-)", line (%d+), characters (%d+)'},chuck={'^(.-)line%((%d+)%)%.char%((%d+)%): (.+)$'},clojure={' error .- at .-%((.-):(%d+)'},cmake={'^CMake Error at (.-):(%d+)','^(.-):(%d+):$'},coffeescript={'^(.-):(%d+):(%d+): (.+)$'},context={'error on line (%d+) in file (.-): (.+)$'},cpp={'^(.-):(%d+):(%d+): (.+)$'},csharp={'^(.-)%((%d+),(%d+)%): (.+)$'},cuda={'^(.-)%((%d+)%): (error.+)$'},dart={"^'(.-)': error: line (%d+) pos (%d+): (.+)$",'%(file://(.-):(%d+):(%d+)%)'},dmd={'^(.-)%((%d+)%): (Error.+)$'},dot={'^Warning: (.-): (.+) in line (%d+)'},eiffel={'^Line (%d+) columns? .- in .- %((.-)%):$','^line (%d+) column (%d+) file (.-)$'},elixir={'^(.-):(%d+): (.+)$','Error%) (.-):(%d+): (.+)$'},erlang={'^(.-):(%d+): (.+)$'},fantom={'^(.-)%((%d+),(%d+)%): (.+)$'},faust={'^(.-):(%d+):(.+)$'},fennel={'^%S+ error in (.-):(%d+)'},forth={'^(.-):(%d+): (.+)$'},fortran={'^(.-):(%d+)%D+(%d+):%s*(.*)$'},fsharp={'^(.-)%((%d+),(%d+)%): (.+)$'},gap={'^(.+) in (.-) line (%d+)$'},gnuplot={'^"(.-)", line (%d+): (.+)$'},go={'^(.-):(%d+):(%d+): (.+)$'},groovy={'^%s+at .-%((.-):(%d+)%)$','^(.-):(%d+): (.+)$'},haskell={'^(.-):(%d+):(%d+):%s*(.*)$'},icon={'^File (.-); Line (%d+) # (.+)$','^.-from line (%d+) in (.-)$'},java={'^%s+at .-%((.-):(%d+)%)$','^(.-):(%d+): (.+)$'},javascript={'^%s+at .-%((.-):(%d+):(%d+)%)$','^%s+at (.-):(%d+):(%d+)$','^(.-):(%d+):?$'},jq={'^jq: error: (.+) at (.-), line (%d+)'},julia={'^%s+%[%d+%].- at (.-):(%d+)$'},ltx={'^(.-):(%d+): (.+)$'},less={'^(.+) in (.-) on line (%d+), column (%d+):$'},lilypond={'^(.-):(%d+):(%d+):%s*(.*)$'},litcoffee={'^(.-):(%d+):(%d+): (.+)$'},lua={'^luac?: (.-):(%d+): (.+)$'},makefile={'^(.-):(%d+): (.+)$'},nemerle={'^(.-)%((%d+),(%d+)%): (.+)$'},nim={'^(.-)%((%d+), (%d+)%) (%w+:.+)$'},objective_c={'^(.-):(%d+):(%d+): (.+)$'},pascal={'^(.-)%((%d+),(%d+)%) (%w+:.+)$'},perl={'^(.+) at (.-) line (%d+)'},php={'^(.+) in (.-) on line (%d+)$'},pike={'^(.-):(%d+):(.+)$'},pony={'^(.-):(%d+):(%d+): (.+)$'},prolog={'^(.-):(%d+):(%d+): (.+)$','^(.-):(%d+): (.+)$'},pure={'^(.-), line (%d+): (.+)$'},python={'^%s*File "(.-)", line (%d+)'},rexx={'^Error %d+ running "(.-)", line (%d+): (.+)$'},ruby={'^%s+from (.-):(%d+):','^(.-):(%d+):%s*(.+)$'},rust={'^(.-):(%d+):(%d+): (.+)$',"panicked at '([^']+)', (.-):(%d+)"},sass={'^WARNING on line (%d+) of (.-):$','^%s+on line (%d+) of (.-)$'},scala={'^%s+at .-%((.-):(%d+)%)$','^(.-):(%d+): (.+)$'},sh={'^(.-): (%d+): %1: (.+)$'},bash={'^(.-): line (%d+): (.+)$'},zsh={'^(.-):(%d+): (.+)$'},smalltalk={'^(.-):(%d+): (.+)$','%((.-):(%d+)%)$'},snobol4={'^(.-):(%d+): (.+)$'},tcl={'^%s*%(file "(.-)" line (%d+)%)$'},tex={'^(.-):(%d+): (.+)$'},typescript={'^(.-):(%d+):(%d+) %- (.+)$'},vala={'^(.-):(%d+)%.(%d+)[%-%.%d]+: (.+)$','^(.-):(%d+):(%d+): (.+)$'},vb={'^(.-)%((%d+),(%d+)%): (.+)$'},xs={'^(.-):(%d+)%S* (.+)$'},zig={'^(.-):(%d+):(%d+): (.+)$'}}
-- Note: APDL,IDL,REBOL,RouterOS,Spin,Verilog,VHDL are proprietary.
-- Note: ASP,CSS,Desktop,diff,django,elm,fstab,gettext,Gtkrc,HTML,ini,JSON,JSP,Markdown,Networkd,Postscript,Properties,R,Reason,RHTML,Systemd,XML don't have parse-able errors.
-- Note: Batch,BibTeX,ConTeXt,Dockerfile,GLSL,Inform,Io,Lisp,MoonScript,Scheme,SQL,TeX cannot be parsed for one reason or another.
-- LuaFormatter on

---
-- Jumps to the source of the recognized compile/run warning or error on line number *line_num*
-- in the message buffer.
-- If *line_num* is `nil`, jumps to the next or previous warning or error, depending on boolean
-- *next*. Displays an annotation with the warning or error message if possible.
-- @param line_num Optional line number in the message buffer that contains the compile/run
--   warning or error to go to. This parameter may be omitted completely.
-- @param next Optional flag indicating whether to go to the next recognized warning/error or
--   the previous one. Only applicable when *line_num* is `nil`.
-- @see error_patterns
-- @name goto_error
function M.goto_error(line_num, next)
  if type(line_num) == 'boolean' then line_num, next = nil, line_num end
  local msg_view, msg_buf = nil, nil
  for i = 1, #_VIEWS do
    if is_msg_buf(_VIEWS[i].buffer) then
      msg_view = _VIEWS[i]
      break
    end
  end
  for i = 1, #_BUFFERS do
    if is_msg_buf(_BUFFERS[i]) then
      msg_buf = _BUFFERS[i]
      break
    end
  end
  if not msg_view and not msg_buf then return end
  if msg_view then
    ui.goto_view(msg_view)
  else
    view:goto_buffer(msg_buf)
  end

  -- If no line number was given, find the next warning or error marker.
  if not assert_type(line_num, 'number/nil', 1) and next ~= nil then
    local f = next and buffer.marker_next or buffer.marker_previous
    line_num = buffer:line_from_position(buffer.current_pos)
    local WARN_BIT, ERROR_BIT = 1 << M.MARK_WARNING - 1, 1 << M.MARK_ERROR - 1
    local wrapped = false
    ::retry::
    local wline = f(buffer, line_num + (next and 1 or -1), WARN_BIT)
    local eline = f(buffer, line_num + (next and 1 or -1), ERROR_BIT)
    if wline == -1 and eline == -1 then
      wline = f(buffer, next and 1 or buffer.line_count, WARN_BIT)
      eline = f(buffer, next and 1 or buffer.line_count, ERROR_BIT)
    elseif wline == -1 or eline == -1 then
      if wline == -1 then
        wline = eline
      else
        eline = wline
      end
    end
    line_num = (next and math.min or math.max)(wline, eline)
    if line_num == -1 and not wrapped then
      line_num = next and 1 or buffer.line_count
      wrapped = true
      goto retry
    end
  end

  -- Goto the warning or error and show an annotation.
  local line = buffer:get_line(line_num):match('^[^\r\n]*')
  local detail = scan_for_error(line:iconv(_CHARSET, 'UTF-8'))
  if not detail then return end
  buffer:goto_line(line_num)
  textadept.editing.select_line()
  if not detail.filename:find(not WIN32 and '^/' or '^%a:[/\\]') and cwd then
    detail.filename = cwd .. (not WIN32 and '/' or '\\') .. detail.filename
  end
  local sloppy = not detail.filename:find(not WIN32 and '^/' or '^%a:[/\\]')
  ui.goto_file(detail.filename, true, preferred_view, sloppy)
  textadept.editing.goto_line(detail.line)
  if detail.column then buffer:goto_pos(buffer:find_column(detail.line, detail.column)) end
  if not detail.message then return end
  buffer.annotation_text[detail.line] = detail.message
  if detail.warning then return end
  buffer.annotation_style[detail.line] = buffer:style_of_name('error')
end
events.connect(events.KEYPRESS, function(code)
  if keys.KEYSYMS[code] == '\n' and is_msg_buf(buffer) and
    scan_for_error(buffer:get_cur_line():match('^[^\r\n]*')) then
    M.goto_error(buffer:line_from_position(buffer.current_pos))
    return true
  end
end)
events.connect(events.DOUBLE_CLICK,
  function(_, line) if is_msg_buf(buffer) then M.goto_error(line) end end)

return M