howl.inspect

local buffer, idle_inspector, save_inspector

before_each ->
  mode.register name: 'inspect-mode', create: -> {}
  idle_inspector = spy.new -> {}
  save_inspector = spy.new -> {}
  inspection.register name: 'test-idle-inspector', factory: -> idle_inspector
  inspection.register name: 'test-save-inspector', factory: -> save_inspector
  buffer = Buffer mode.by_name('inspect-mode')

after_each ->
  mode.unregister 'inspect-mode'
  inspection.unregister 'test-idle-inspector'
  inspection.unregister 'test-save-inspector'

inspect(buffer, scope)

runs inspectors specified for the buffer

buffer.config.inspectors_on_idle = {'test-idle-inspector'}
inspect.inspect(buffer)
assert.spy(idle_inspector).was_called_with(match.is_ref(buffer))

runs inspectors specified for the mode

buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
inspect.inspect(buffer)
assert.spy(idle_inspector).was_called_with(match.is_ref(buffer))

merges inspection results into one scathing result

inspection.register name: 'inspector1', factory: ->
  -> { { line: 1, type: 'error', message: 'foo' } }

inspection.register name: 'inspector2', factory: ->
  -> {
    { line: 1, type: 'error', message: 'foo_mode' }
    { line: 3, type: 'warning', message: 'bar' }
  }

buffer.config.inspectors_on_idle = {'inspector1', 'inspector2'}
res = inspect.inspect(buffer)
assert.same {
  [1]: {
    { type: 'error', message: 'foo' },
    { type: 'error', message: 'foo_mode' }
  }
  [3]: {
    { type: 'warning', message: 'bar' }
  }
 }, res

(inspector types)

before_each ->
  with buffer.mode.config
    .inspectors_on_idle = {'test-idle-inspector'}
    .inspectors_on_save = {'test-save-inspector'}

runs both idle and save inspectors by default

inspect.inspect(buffer)
assert.spy(idle_inspector).was_called_with(match.is_ref(buffer))
assert.spy(save_inspector).was_called_with(match.is_ref(buffer))

runs only idle inspectors if specified

inspect.inspect(buffer, scope: 'idle')
assert.spy(idle_inspector).was_called_with(match.is_ref(buffer))
assert.spy(save_inspector).was_not_called!

runs only save inspectors if specified

inspect.inspect(buffer, scope: 'save')
assert.spy(save_inspector).was_called_with(match.is_ref(buffer))
assert.spy(idle_inspector).was_not_called!

(when the returned inspector is a string)

it 'is run as an external command, translating default output parsing', (done) ->

idle_inspector = 'echo "foo:1: warning: foo\nline 2: wrong val \\`foo\\`"'
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.same {
    [1]: {
      { message: 'warning: foo', type: 'warning' },
    }
    [2]: {
      { message: 'wrong val `foo`', search: 'foo' }
    }
   }, res
  done!

it 'ignores output for other files', (done) ->

buffer.file = File '/my/file'
idle_inspector = 'echo "/my/file:1: warning: foo\n/other/file:2: wrong val \\`foo\\`"'
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.same {
    [1]: {
      { message: 'warning: foo', type: 'warning' },
    }
   }, res
  done!

it 'keeps output for entries without a file', (done) ->

buffer.file = File '/my/file'
idle_inspector = 'echo 1: foo'
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.same {
    [1]: {
      { message: 'foo' },
    }
   }, res
  done!

(when the returned inspector is a table)

it 'uses the `cmd` key as the external command to run', (done) ->

idle_inspector = cmd: 'echo "foo:1: some warning"'
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.same {
    [1]: {
      { message: 'some warning' },
    }
   }, res
  done!

it 'allows for custom parsing via the `parse` key', (done) ->

idle_inspector = {
  cmd: 'echo "output"'
  parse: spy.new -> { {line: 1, message: 'foo' } }
}
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.spy(idle_inspector.parse).was_called_with('output\n')
  assert.same {
    [1]: {
      { message: 'foo' },
    }
   }, res
  done!

it 'allows for custom post processing via the `post_parse` key', (done) ->

idle_inspector = {
  cmd: 'echo "foo:1: some warning"'
  post_parse: (inspections) -> inspections[1].search = 'zed'
}
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.same {
    [1]: {
      { message: 'some warning', search: 'zed' },
    }
   }, res
  done!

it 'allows for specifying the type of inspections via the `type` key', (done) ->

idle_inspector = cmd: 'echo "foo:1: some warning"', type: 'warning'
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.same {
    [1]: {
      { message: 'some warning', type: 'warning' },
    }
   }, res
  done!

it 'skips unavailable inspectors', (done) ->

idle_inspector = {
  cmd: 'echo "foo:1: some warning"',
  is_available: -> false
}
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.same {}, res
  done!

(when an inspector command contains a <file> placeholder)

it "is skipped if the buffer has no associated file", (done) ->

buffer.modified = false
idle_inspector = 'echo "foo:1: <file> urk"'
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  assert.same {}, inspect.inspect(buffer)
  done!

it "is skipped if the buffer is modified", (done) ->

file = File '/foo/bar'
buffer.file = file
buffer.modified = true
idle_inspector = 'echo "foo:1: <file> urk"'
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  assert.same {}, inspect.inspect(buffer)
  done!

it "is expanded with the buffer's file's path", (done) ->

file = File '/foo/bar'
buffer.file = file
idle_inspector = cmd: 'echo "/foo/bar:1: <file>"'
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.same {
    [1]: {
      { message: '/foo/bar' },
    }
  }, res
  done!

it 'is not passed the buffer content on stdin', (done) ->

file = File '/foo/bar'
buffer.file = file
buffer.text = 'bar:1: mem'
buffer.modified = false
idle_inspector = cmd: 'cat <file> -'
howl_async ->
  buffer.mode.config.inspectors_on_idle = {'test-idle-inspector'}
  res = inspect.inspect(buffer)
  assert.same {}, res
  done!

criticize(buffer, criticism, opts)

before_each ->
  buffer.text = 'linƏ 1\nline 2\nline 3'

applies inspect markers to the buffer corresponding to criticisms

inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'bar'}
  },
  [2]: {
    {type: 'error', message: 'zed'}
  }
 }
assert.same {
  {
    start_offset: 1,
    end_offset: 7,
    name: 'inspection',
    flair: 'error',
    message: 'bar'
  },
  {
    start_offset: 8,
    end_offset: 14,
    name: 'inspection',
    flair: 'error',
    message: 'zed'
  }
}, buffer.markers.all

leaves previous inspection markers alone by default

inspect.criticize buffer, {
  [1]: { {type: 'error', message: 'bar'} }
}

inspect.criticize buffer, {
  [2]: { {type: 'error', message: 'zed'} }
}

assert.equal 2, #buffer.markers.all

clears previous inspection markers when opts.clear is set

inspect.criticize buffer, {
  [1]: { {type: 'error', message: 'bar'} }
}

inspect.criticize buffer, {
  [2]: { {type: 'error', message: 'zed'} }
}, clear: true

assert.equal 1, #buffer.markers.all
assert.equal 8, buffer.markers.all[1].start_offset

starts the visual marker at the start of text for line inspections

buffer.text = '  34567\n'
inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'zed'}
  }
}
assert.equal 3, buffer.markers.all[1].start_offset

starts the visual marker at the unicode pos given by .start_col if present

buffer.text = 'åäö\n'
inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'zed', start_col: 2}
  }
}
assert.equal 2, buffer.markers.all[1].start_offset

ends the visual marker at the unicode pos given by .end_col if present

buffer.text = 'åäö\n'
inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'zed', end_col: 3}
  }
}
assert.equal 3, buffer.markers.all[1].end_offset

starts the visual marker at the byte offset given by .byte_start_col if present

buffer.text = 'åäö\n'
inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'zed', byte_start_col: 3}
  }
}
assert.equal 2, buffer.markers.all[1].start_offset

ends the visual marker at the byte offset given by .byte_end_col if present

buffer.text = 'åäö\n'
inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'zed', byte_end_col: 3}
  }
}
assert.equal 2, buffer.markers.all[1].end_offset

when a .search field is present

is used for selecting a part of the line to highlight

buffer.text = '1 345 7\n'
inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'zed', search: '345'}
  }
}
marker = buffer.markers.all[1]
assert.equal 3, marker.start_offset
assert.equal 6, marker.end_offset

marks the whole line if the search fails

buffer.text = '1234567\n'
inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'zed', search: 'XX'}
  }
}
marker = buffer.markers.all[1]
assert.equal 1, marker.start_offset
assert.equal 8, marker.end_offset

marks the whole line if the search has multiple matches

buffer.text = 'foo foo\n'
inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'zed', search: 'oo'}
  }
}
marker = buffer.markers.all[1]
assert.equal 1, marker.start_offset
assert.equal 8, marker.end_offset

is not confused by other substring matches

buffer.text = ' res = refresh!\n'
inspect.criticize buffer, {
  [1]: {
    {type: 'error', message: 'zed', search: 'res'}
  }
}
marker = buffer.markers.all[1]
assert.equal 2, marker.start_offset
assert.equal 5, marker.end_offset

publish(buffer, source, items)

before_each ->
  buffer.text = 'åäö 1\nline 2\nline 3'

lsp_markers = -> buffer.markers\find name: 'inspection', source: 'lsp'

marks the items, tagged with the source

inspect.publish buffer, 'lsp', {
  { line: 1, byte_start_col: 3, byte_end_col: 7, type: 'warning', message: 'bar' }
}
assert.same {
  {
    start_offset: 2,
    end_offset: 4,
    name: 'inspection',
    flair: 'warning',
    message: 'bar',
    source: 'lsp'
  }
}, buffer.markers.all

ends the marker on .end_line if given, clamped to the last line

inspect.publish buffer, 'lsp', {
  { line: 1, byte_start_col: 1, end_line: 2, byte_end_col: 3, message: 'a' }
  { line: 2, byte_start_col: 1, end_line: 9, byte_end_col: 3, message: 'b' }
}
markers = lsp_markers!
assert.same {1, 9}, {markers[1].start_offset, markers[1].end_offset}
assert.same {7, 16}, {markers[2].start_offset, markers[2].end_offset}

clamps byte columns to the line

inspect.publish buffer, 'lsp', {
  { line: 2, byte_start_col: 3, byte_end_col: 42, message: 'a' }
}
marker = lsp_markers![1]
assert.same {9, 13}, {marker.start_offset, marker.end_offset}

widens empty ranges to one character

inspect.publish buffer, 'lsp', {
  { line: 2, byte_start_col: 3, byte_end_col: 3, message: 'a' }
  { line: 2, byte_start_col: 7, byte_end_col: 7, message: 'b' }
}
markers = lsp_markers!
assert.same {9, 10}, {markers[1].start_offset, markers[1].end_offset}
assert.same {12, 13}, {markers[2].start_offset, markers[2].end_offset}

replaces previous markers from the same source only

inspect.criticize buffer, { [1]: { {message: 'pulled'} } }
inspect.publish buffer, 'other', { {line: 3, message: 'other'} }
inspect.publish buffer, 'lsp', { {line: 1, message: 'old'} }
inspect.publish buffer, 'lsp', { {line: 2, message: 'new'} }
messages = [m.message for m in *buffer.markers.all]
table.sort messages
assert.same {'new', 'other', 'pulled'}, messages

clears the markers from the source when given no items

inspect.publish buffer, 'lsp', { {line: 1, message: 'old'} }
inspect.publish buffer, 'lsp', {}
assert.same {}, buffer.markers.all

adds no markers when auto_inspect is off

buffer.config.auto_inspect = 'off'
inspect.publish buffer, 'lsp', { {line: 1, message: 'old'} }
assert.same {}, buffer.markers.all

the cursor-goto-inspection command

local editor, select_opts

before_each ->
  editor = howl.app.editor
  howl.app.editor = :buffer
  howl.interact.select_location = (opts) -> select_opts = opts

after_each ->
  howl.app.editor = editor
  howl.interact.select_location = nil

lists the first line of the message for each inspection

buffer.text = 'line 1\nline 2'
inspect.publish buffer, 'lsp', {
  { line: 2, byte_start_col: 1, byte_end_col: 3, message: 'first\nmore' }
  { line: 2, byte_start_col: 4, byte_end_col: 5, message: 'second' }
}
howl.command.get('cursor-goto-inspection').input {}
rows = [{item[1], tostring(item[2]), item[3]} for item in *select_opts.items]
assert.same { {'2', 'line 2', 'first'}, {'·', 'line 2', 'second'} }, rows