From b5043d529f61a24f5548b0a801d4068c07d09deb Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Mon, 5 Sep 2016 12:36:49 -0600 Subject: [PATCH 1/5] Add support for filling holes using ghc-mod's auto command --- README.md | 1 + lib/ghc-mod/ghc-modi-process.coffee | 21 +++++++++++++++++++ lib/upi-consumer.coffee | 20 ++++++++++++++++++ lib/views/suggestion-list-view.coffee | 30 +++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 lib/views/suggestion-list-view.coffee diff --git a/README.md b/README.md index eb31154..c78346a 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ You can edit Atom keybindings by opening 'Edit → Open Your Keymap'. Here is a 'ctrl-alt-i': 'haskell-ghc-mod:show-info' #this is an example binding 'ctrl-alt-T': 'haskell-ghc-mod:insert-type' #this is an example binding '': 'haskell-ghc-mod:case-split' + '': 'haskell-ghc-mod:hole-fill' '': 'haskell-ghc-mod:sig-fill' '': 'haskell-ghc-mod:show-info-fallback-to-type' '': 'haskell-ghc-mod:show-type-fallback-to-info' diff --git a/lib/ghc-mod/ghc-modi-process.coffee b/lib/ghc-mod/ghc-modi-process.coffee index 7f0a4ab..2bd00e3 100644 --- a/lib/ghc-mod/ghc-modi-process.coffee +++ b/lib/ghc-mod/ghc-modi-process.coffee @@ -338,6 +338,27 @@ class GhcModiProcess ] replacement: text + doHoleFill: (buffer, crange) => + return Promise.resolve [] unless buffer.getUri()? + crange = Util.tabShiftForRange(buffer, crange) + @queueCmd 'typeinfo', + interactive: @caps?.interactiveCaseSplit ? false + buffer: buffer + command: 'auto', + uri: buffer.getUri() + text: buffer.getText() if buffer.isModified() + args: [crange.start.row + 1, crange.start.column + 1] + .then (lines) -> + return null if lines.length == 0 or lines[1] == "" + + [line_, rowstart, colstart, rowend, colend, text] = lines[0].match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/) + + range: Range.fromObject [ + [parseInt(rowstart) - 1, parseInt(colstart) - 1], + [parseInt(rowend) - 1, parseInt(colend) - 1] + ] + suggestions: lines[1..] + doSigFill: (buffer, crange) => return Promise.resolve [] unless buffer.getUri()? crange = Util.tabShiftForRange(buffer, crange) diff --git a/lib/upi-consumer.coffee b/lib/upi-consumer.coffee index 2c0f570..8eb7a42 100644 --- a/lib/upi-consumer.coffee +++ b/lib/upi-consumer.coffee @@ -1,5 +1,6 @@ {CompositeDisposable} = require 'atom' ImportListView = require './views/import-list-view' +SuggestionListView = require './views/suggestion-list-view' module.exports = class UPIConsumer @@ -24,6 +25,7 @@ class UPIConsumer 'haskell-ghc-mod:show-type': @tooltipCommand @typeTooltip 'haskell-ghc-mod:show-info': @tooltipCommand @infoTooltip 'haskell-ghc-mod:case-split': @caseSplitCommand + 'haskell-ghc-mod:hole-fill': @holeFillCommand 'haskell-ghc-mod:sig-fill': @sigFillCommand 'haskell-ghc-mod:go-to-declaration': @goToDeclCommand 'haskell-ghc-mod:show-info-fallback-to-type': @tooltipCommand @infoTypeTooltip @@ -40,6 +42,7 @@ class UPIConsumer {label: 'Show Info', command: 'haskell-ghc-mod:show-info'} {label: 'Show Type And Info', command: 'haskell-ghc-mod:show-type-and-info'} {label: 'Case Split', command: 'haskell-ghc-mod:case-split'} + {label: 'Hole Fill', command: 'haskell-ghc-mod:hole-fill'} {label: 'Sig Fill', command: 'haskell-ghc-mod:sig-fill'} {label: 'Insert Type', command: 'haskell-ghc-mod:insert-type'} {label: 'Insert Import', command: 'haskell-ghc-mod:insert-import'} @@ -146,6 +149,23 @@ class UPIConsumer res.forEach ({range, replacement}) -> editor.setTextInBufferRange(range, replacement) + holeFillCommand: ({target, detail}) => + editor = target.getModel() + @upi.withEventRange {editor, detail}, ({crange}) => + @process.doHoleFill(editor.getBuffer(), crange) + .then (res) -> + return null unless res? + + {range, suggestions} = res + + if suggestions.length == 1 + editor.setTextInBufferRange(range, suggestions[0]) + else + new SuggestionListView + items: suggestions + onConfirmed: (suggestion) -> + editor.setTextInBufferRange(range, suggestion) + sigFillCommand: ({target, detail}) => editor = target.getModel() @upi.withEventRange {editor, detail}, ({crange}) => diff --git a/lib/views/suggestion-list-view.coffee b/lib/views/suggestion-list-view.coffee new file mode 100644 index 0000000..2d1ff93 --- /dev/null +++ b/lib/views/suggestion-list-view.coffee @@ -0,0 +1,30 @@ +{SelectListView} = require 'atom-space-pen-views' + +module.exports= +class SuggestionListView extends SelectListView + initialize: ({@onConfirmed, items}) -> + super + @panel = atom.workspace.addModalPanel + item: this + visible: false + @addClass 'ide-haskell' + @show items + + cancelled: -> + @panel.destroy() + + getFilterKey: -> + "text" + + show: (list) -> + @setItems list + @panel.show() + @storeFocusedElement() + @focusFilterEditor() + + viewForItem: (mod) -> + "
  • #{mod}
  • " + + confirmed: (mod) -> + @onConfirmed? mod + @cancel() From aa6078a82837643d0d6c0265fed2874e9d6b719d Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Fri, 9 Sep 2016 10:01:40 +0300 Subject: [PATCH 2/5] Removed hole-fill command --- README.md | 1 - lib/upi-consumer.coffee | 20 ------------------ lib/views/suggestion-list-view.coffee | 30 --------------------------- 3 files changed, 51 deletions(-) delete mode 100644 lib/views/suggestion-list-view.coffee diff --git a/README.md b/README.md index c78346a..eb31154 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,6 @@ You can edit Atom keybindings by opening 'Edit → Open Your Keymap'. Here is a 'ctrl-alt-i': 'haskell-ghc-mod:show-info' #this is an example binding 'ctrl-alt-T': 'haskell-ghc-mod:insert-type' #this is an example binding '': 'haskell-ghc-mod:case-split' - '': 'haskell-ghc-mod:hole-fill' '': 'haskell-ghc-mod:sig-fill' '': 'haskell-ghc-mod:show-info-fallback-to-type' '': 'haskell-ghc-mod:show-type-fallback-to-info' diff --git a/lib/upi-consumer.coffee b/lib/upi-consumer.coffee index 8eb7a42..2c0f570 100644 --- a/lib/upi-consumer.coffee +++ b/lib/upi-consumer.coffee @@ -1,6 +1,5 @@ {CompositeDisposable} = require 'atom' ImportListView = require './views/import-list-view' -SuggestionListView = require './views/suggestion-list-view' module.exports = class UPIConsumer @@ -25,7 +24,6 @@ class UPIConsumer 'haskell-ghc-mod:show-type': @tooltipCommand @typeTooltip 'haskell-ghc-mod:show-info': @tooltipCommand @infoTooltip 'haskell-ghc-mod:case-split': @caseSplitCommand - 'haskell-ghc-mod:hole-fill': @holeFillCommand 'haskell-ghc-mod:sig-fill': @sigFillCommand 'haskell-ghc-mod:go-to-declaration': @goToDeclCommand 'haskell-ghc-mod:show-info-fallback-to-type': @tooltipCommand @infoTypeTooltip @@ -42,7 +40,6 @@ class UPIConsumer {label: 'Show Info', command: 'haskell-ghc-mod:show-info'} {label: 'Show Type And Info', command: 'haskell-ghc-mod:show-type-and-info'} {label: 'Case Split', command: 'haskell-ghc-mod:case-split'} - {label: 'Hole Fill', command: 'haskell-ghc-mod:hole-fill'} {label: 'Sig Fill', command: 'haskell-ghc-mod:sig-fill'} {label: 'Insert Type', command: 'haskell-ghc-mod:insert-type'} {label: 'Insert Import', command: 'haskell-ghc-mod:insert-import'} @@ -149,23 +146,6 @@ class UPIConsumer res.forEach ({range, replacement}) -> editor.setTextInBufferRange(range, replacement) - holeFillCommand: ({target, detail}) => - editor = target.getModel() - @upi.withEventRange {editor, detail}, ({crange}) => - @process.doHoleFill(editor.getBuffer(), crange) - .then (res) -> - return null unless res? - - {range, suggestions} = res - - if suggestions.length == 1 - editor.setTextInBufferRange(range, suggestions[0]) - else - new SuggestionListView - items: suggestions - onConfirmed: (suggestion) -> - editor.setTextInBufferRange(range, suggestion) - sigFillCommand: ({target, detail}) => editor = target.getModel() @upi.withEventRange {editor, detail}, ({crange}) => diff --git a/lib/views/suggestion-list-view.coffee b/lib/views/suggestion-list-view.coffee deleted file mode 100644 index 2d1ff93..0000000 --- a/lib/views/suggestion-list-view.coffee +++ /dev/null @@ -1,30 +0,0 @@ -{SelectListView} = require 'atom-space-pen-views' - -module.exports= -class SuggestionListView extends SelectListView - initialize: ({@onConfirmed, items}) -> - super - @panel = atom.workspace.addModalPanel - item: this - visible: false - @addClass 'ide-haskell' - @show items - - cancelled: -> - @panel.destroy() - - getFilterKey: -> - "text" - - show: (list) -> - @setItems list - @panel.show() - @storeFocusedElement() - @focusFilterEditor() - - viewForItem: (mod) -> - "
  • #{mod}
  • " - - confirmed: (mod) -> - @onConfirmed? mod - @cancel() From 940e284aff7a62d8fb01a766acb6e996fcba1da5 Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Fri, 9 Sep 2016 10:33:18 +0300 Subject: [PATCH 3/5] Add timeout support for non-interactive actions --- lib/ghc-mod/ghc-modi-process-real.coffee | 18 +++++++++++++----- lib/util.coffee | 2 ++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/ghc-mod/ghc-modi-process-real.coffee b/lib/ghc-mod/ghc-modi-process-real.coffee index ba102fd..d4d0382 100644 --- a/lib/ghc-mod/ghc-modi-process-real.coffee +++ b/lib/ghc-mod/ghc-modi-process-real.coffee @@ -3,6 +3,7 @@ CP = require('child_process') InteractiveProcess = require './interactive-process' {debug, warn, mkError, withTempFile, EOT} = Util = require '../util' {EOL} = require('os') +_ = require 'underscore-plus' module.exports = class GhcModiProcessReal @@ -10,7 +11,10 @@ class GhcModiProcessReal @disposables = new CompositeDisposable @disposables.add @emitter = new Emitter - run: ({interactive, command, text, uri, dashArgs, args, suppressErrors}) -> + run: ({interactive, command, text, uri, dashArgs, args, suppressErrors, timeout}) -> + if timeout? and interactive + throw new Error('Can not have interactive action with set timeout! This + is an error in haskell-ghc-mod. Please report it.') args ?= [] dashArgs ?= [] if atom.config.get('haskell-ghc-mod.lowMemorySystem') @@ -25,9 +29,9 @@ class GhcModiProcessReal P = if text? and not @caps.fileMap withTempFile text, uri, (tempuri) -> - fun {command, uri: tempuri, args} + fun {command, uri: tempuri, args, timeout} else - fun {command, text, uri, args} + fun {command, text, uri, args, timeout} P.catch (err) => debug err if err.name is 'InteractiveActionTimeout' @@ -44,6 +48,10 @@ class GhcModiProcessReal """ stack: err.stack dismissable: true + return [] + else if err.name is 'NonInteractiveActionTimeout' + warn err + return [] else if not suppressErrors atom.notifications.addFatalError " Haskell-ghc-mod: ghc-mod @@ -77,7 +85,7 @@ class GhcModiProcessReal @proc = null return @proc - runModCmd: ({command, text, uri, args}) => + runModCmd: ({command, text, uri, args, timeout}) => modPath = atom.config.get('haskell-ghc-mod.ghcModPath') result = [] err = [] @@ -88,7 +96,7 @@ class GhcModiProcessReal if text? cmd = ['--map-file', uri].concat cmd stdin = "#{text}#{EOT}" if text? - Util.execPromise modPath, cmd, @options, stdin + Util.execPromise modPath, cmd, _.extend({timeout}, @options), stdin .then (stdout) -> stdout.split(EOL).slice(0, -1).map (line) -> line.replace /\0/g, '\n' diff --git a/lib/util.coffee b/lib/util.coffee index 8cda265..d64df6f 100644 --- a/lib/util.coffee +++ b/lib/util.coffee @@ -58,6 +58,8 @@ module.exports = Util = Util.warn("Running #{cmd} #{args} failed with ", error) Util.warn stdout if stdout error.stack = (new Error).stack + if child.killed and opts.timeout? + error.name = 'NonInteractiveActionTimeout' reject error else Util.debug "Got response from #{cmd} #{args}", stdout: stdout, stderr: stderr From 487801404933154dc359ac7029b057dd9e52aaf9 Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Fri, 9 Sep 2016 10:46:15 +0300 Subject: [PATCH 4/5] Add timeout to doHoleFill Also some minor tweaks --- lib/ghc-mod/ghc-modi-process.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ghc-mod/ghc-modi-process.coffee b/lib/ghc-mod/ghc-modi-process.coffee index 2bd00e3..61ff836 100644 --- a/lib/ghc-mod/ghc-modi-process.coffee +++ b/lib/ghc-mod/ghc-modi-process.coffee @@ -342,14 +342,15 @@ class GhcModiProcess return Promise.resolve [] unless buffer.getUri()? crange = Util.tabShiftForRange(buffer, crange) @queueCmd 'typeinfo', - interactive: @caps?.interactiveCaseSplit ? false + interactive: false buffer: buffer - command: 'auto', + command: 'auto' uri: buffer.getUri() text: buffer.getText() if buffer.isModified() args: [crange.start.row + 1, crange.start.column + 1] + timeout: 5000 # TODO: Make configurable .then (lines) -> - return null if lines.length == 0 or lines[1] == "" + return {} if lines.length == 0 or lines[1] == "" [line_, rowstart, colstart, rowend, colend, text] = lines[0].match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/) From ecb7b467414f04d9d893cbfebaa84a73e20bc785 Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Fri, 9 Sep 2016 10:46:36 +0300 Subject: [PATCH 5/5] Add hole-fill suggestions to getCompletionsForHole --- .../completion-backend.coffee | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/completion-backend/completion-backend.coffee b/lib/completion-backend/completion-backend.coffee index 6517e81..4bcbffb 100644 --- a/lib/completion-backend/completion-backend.coffee +++ b/lib/completion-backend/completion-backend.coffee @@ -343,7 +343,7 @@ class CompletionBackend position = Range.fromPointWithDelta(position, 0, 0) if position? prefix = prefix.slice 1 if prefix.startsWith '_' @process.getTypeInBuffer(buffer, position).then ({type}) => - @getSymbolsForBuffer(buffer).then (symbols) -> + @getSymbolsForBuffer(buffer).then (symbols) => ts = symbols.filter (s) -> return false unless s.typeSignature? tl = s.typeSignature.split(' -> ').slice(-1)[0] @@ -351,8 +351,18 @@ class CompletionBackend ts = tl.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&") rx = RegExp ts.replace(/\b[a-z]\b/g, '.+'), '' rx.test(type) - if prefix.length is 0 - ts.sort (a, b) -> - FZ.score(b.typeSignature, type) - FZ.score(a.typeSignature, type) - else - FZ.filter ts, prefix, key: 'qname' + ts2 = + if prefix.length is 0 + ts.sort (a, b) -> + FZ.score(b.typeSignature, type) - FZ.score(a.typeSignature, type) + else + FZ.filter ts, prefix, key: 'qname' + @process.doHoleFill(buffer, position).then ({suggestions}) -> + return ts2 unless suggestions? + ts2.unshift (suggestions.map (text) -> + name: text + qname: text + typeSignature: type + symbolType: 'snippet' + )... + return ts2