diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e6ee2..2cbc38d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Added +- Added a REPL [#80](https://github.com/idris-hackers/atom-language-idris/pull/80) +- Added a panel for the `apropos` command + ### Fixed ## v0.3.4 diff --git a/keymaps/language-idris.json b/keymaps/language-idris.json index 63964a2..c52549b 100644 --- a/keymaps/language-idris.json +++ b/keymaps/language-idris.json @@ -5,6 +5,7 @@ "ctrl-alt-a": "language-idris:add-clause", "ctrl-alt-s": "language-idris:proof-search", "ctrl-alt-d": "language-idris:docs-for", + "ctrl-alt-enter": "language-idris:open-repl", "ctrl-alt-w": "language-idris:make-with", "ctrl-alt-l": "language-idris:make-lemma", "ctrl-alt-r": "language-idris:typecheck", diff --git a/lib/idris-controller.coffee b/lib/idris-controller.coffee index 393e718..d9dd42c 100644 --- a/lib/idris-controller.coffee +++ b/lib/idris-controller.coffee @@ -23,6 +23,8 @@ class IdrisController 'language-idris:typecheck': @runCommand @typecheckFile 'language-idris:print-definition': @runCommand @printDefinition 'language-idris:stop-compiler': @stopCompiler + 'language-idris:open-repl': @runCommand @openREPL + 'language-idris:apropos': @runCommand @apropos isIdrisFile: (uri) -> uri?.match? /\.idr$/ @@ -337,6 +339,38 @@ class IdrisController .printDefinition word .subscribe successHandler, @displayErrors + openREPL: ({ target }) => + editor = target.model + uri = editor.getURI() + + successHandler = ({ responseType, msg }) -> + options = + split: 'right' + searchAllPanes: true + + atom.workspace.open "idris://repl", options + + @model + .load uri + .filter ({ responseType }) -> responseType == 'return' + .subscribe successHandler, @displayErrors + + apropos: ({ target }) => + editor = target.model + uri = editor.getURI() + + successHandler = ({ responseType, msg }) -> + options = + split: 'right' + searchAllPanes: true + + atom.workspace.open "idris://apropos", options + + @model + .load uri + .filter ({ responseType }) -> responseType == 'return' + .subscribe successHandler, @displayErrors + displayErrors: (err) => @messages.show() @messages.clear() diff --git a/lib/idris-model.coffee b/lib/idris-model.coffee index dff337b..b88ac43 100644 --- a/lib/idris-model.coffee +++ b/lib/idris-model.coffee @@ -48,6 +48,7 @@ class IdrisModel subject.onError message: ret[1] warnings: @warnings[id] + highlightInformation: ret[2] subject.onCompleted() delete @subjects[id] when ':write-string' @@ -127,4 +128,7 @@ class IdrisModel printDefinition: (name) -> @prepareCommand [':print-definition', name] + apropos: (name) -> + @prepareCommand [':apropos', name] + module.exports = IdrisModel diff --git a/lib/language-idris.coffee b/lib/language-idris.coffee index d5d49be..07aa7ca 100644 --- a/lib/language-idris.coffee +++ b/lib/language-idris.coffee @@ -1,5 +1,7 @@ IdrisController = require './idris-controller' { CompositeDisposable } = require 'atom' +url = require 'url' +{ IdrisPanel } = require './views/panel-view' module.exports = config: @@ -28,6 +30,16 @@ module.exports = @subscriptions = new CompositeDisposable @subscriptions.add subscription + atom.workspace.addOpener (uriToOpen, options) => + try + { protocol, host, pathname } = url.parse uriToOpen + catch error + return + + return unless protocol is 'idris:' + + new IdrisPanel @controller, host + deactivate: -> @subscriptions.dispose() this.controller.destroy() diff --git a/lib/utils/dom.coffee b/lib/utils/dom.coffee index 533a3e7..1d3d06c 100644 --- a/lib/utils/dom.coffee +++ b/lib/utils/dom.coffee @@ -16,6 +16,28 @@ createCodeElement = -> pre.style.webkitFontFeatureSettings = '"liga"' pre +fontOptions = -> + fontSize = atom.config.get 'language-idris.panelFontSize' + fontSizeAttr = "#{fontSize}px" + enableLigatures = atom.config.get 'language-idris.panelFontLigatures' + webkitFontFeatureSettings = + if enableLigatures + '"liga"' + else + '"inherit"' + + fontFamily = atom.config.get 'language-idris.panelFontFamily' + if fontFamily != '' + fontFamily + else + '"inherit"' + + 'font-size': fontSizeAttr + '-webkit-font-feature-settings': webkitFontFeatureSettings + 'font-family': fontFamily + + module.exports = joinHtmlElements: joinHtmlElements createCodeElement: createCodeElement + fontOptions: fontOptions diff --git a/lib/utils/highlighter.coffee b/lib/utils/highlighter.coffee index 4788b70..8ce7057 100644 --- a/lib/utils/highlighter.coffee +++ b/lib/utils/highlighter.coffee @@ -1,6 +1,8 @@ # Applies the highlighting we get from the idris compiler to our source code. # http://docs.idris-lang.org/en/latest/reference/ide-protocol.html#output-highlighting +CycleDOM = require '@cycle/dom' + highlightInfoListToOb = (list) -> obj = { } for x in list @@ -21,8 +23,18 @@ decorToClasses = (decor) -> else [] highlightWord = (word, info) -> + type = info.info.type || "" + doc = info.info['doc-overview'] || "" + + description = + if info.info.type? + "#{type}\n\n#{doc}".trim() + else + "" + classes: decorToClasses(info.info.decor).concat 'idris' word: word + description: description # Build highlighting information that we can then pass to one # of our serializers. @@ -80,8 +92,15 @@ highlightToHtml = (highlights) -> container.appendChild span container +highlightToCycle = (highlights) -> + highlights.map ({ classes, word, description }) -> + if classes.length == 0 + word + else + CycleDOM.h 'span', { className: classes.join(' '), title: description }, word module.exports = highlight: highlight highlightToString: highlightToString highlightToHtml: highlightToHtml + highlightToCycle: highlightToCycle diff --git a/lib/utils/sexp-formatter.coffee b/lib/utils/sexp-formatter.coffee index eabd8ff..65d7027 100644 --- a/lib/utils/sexp-formatter.coffee +++ b/lib/utils/sexp-formatter.coffee @@ -27,7 +27,7 @@ formatSexp = (sexp) -> else if isSymbol sexp sexp else if isString sexp - '"' + sexp + '"' + '"' + sexp.trim() + '"' else if isBoolean sexp if sexp ':True' diff --git a/lib/views/apropos-view.coffee b/lib/views/apropos-view.coffee new file mode 100644 index 0000000..28b4a8b --- /dev/null +++ b/lib/views/apropos-view.coffee @@ -0,0 +1,80 @@ +Cycle = require '@cycle/core' +CycleDOM = require '@cycle/dom' +highlighter = require '../utils/highlighter' +Rx = require 'rx-lite' +{ fontOptions } = require '../utils/dom' + +styles = fontOptions() + +AproposCycle = + # highlight : forall a. + # { code : String, highlightInformation : HighlightInformation } -> + # CycleDOM + highlight: ({ code, highlightInformation }) -> + highlights = highlighter.highlight code, highlightInformation + highlighter.highlightToCycle highlights + + # view : Observable State -> Observable CycleDOM + view: (state$) -> + state$.map (apropos) -> + aproposAnswer = + if apropos.code + highlightedCode = AproposCycle.highlight apropos + CycleDOM.h 'pre', { className: 'idris-apropos-output', style: styles }, highlightedCode + else + CycleDOM.h 'span', '' + + CycleDOM.h 'div', + { + className: 'idris-panel-view' + }, + [ + CycleDOM.h 'input', { type: 'text', className: 'native-key-bindings idris-repl-input-field' }, '' + CycleDOM.h 'div', aproposAnswer + ] + + main: (responses) -> + input = responses.DOM.select('input').events('keydown') + .filter (ev) -> ev.keyCode == 13 + .map (ev) -> ev.target.value + .startWith '' + + DOM: AproposCycle.view responses.CONTENT + CONTENT: input + + # driver : forall a. + # IdrisModel -> Observable String -> + # Observable (List { a | code : String, highlightInformation : highlightInformation }) + driver: + (options) -> + DOM: CycleDOM.makeDOMDriver options.hostElement + CONTENT: (inp) -> + inp + .filter (line) -> line != '' + .flatMap (line) -> + escapedLine = line.replace(/"/g, '\\"') + options.model.apropos escapedLine + .map (e) -> + code: e.msg[0] + highlightInformation: e.msg[1] + .catch (e) -> + Rx.Observable.just + code: e.message + highlightInformation: e.highlightInformation + .startWith { } + +class AproposView + constructor: (params) -> + hostElement = document.createElement 'div' + @[0] = hostElement + + model = params.controller.model + + drivers = + AproposCycle.driver + hostElement: hostElement + model: model + + Cycle.run AproposCycle.main, drivers + +module.exports = AproposView diff --git a/lib/views/panel-view.coffee b/lib/views/panel-view.coffee new file mode 100644 index 0000000..230df85 --- /dev/null +++ b/lib/views/panel-view.coffee @@ -0,0 +1,24 @@ +REPLView = require './repl-view' +AproposView = require './apropos-view' + +class IdrisPanel + constructor: (@controller, @panel) -> + + getTitle: -> + switch @panel + when "repl" then "Idris: REPL" + when "apropos" then "Idris: Apropos" + else "Idris ?" + + getViewClass: -> + switch @panel + when "repl" then REPLView + when "apropos" then AproposView + + getURI: -> + switch @panel + when "repl" then "idris://repl" + when "apropos" then "idris://apropos" + +module.exports = + IdrisPanel: IdrisPanel diff --git a/lib/views/repl-view.coffee b/lib/views/repl-view.coffee new file mode 100644 index 0000000..85a10d6 --- /dev/null +++ b/lib/views/repl-view.coffee @@ -0,0 +1,119 @@ +Cycle = require '@cycle/core' +CycleDOM = require '@cycle/dom' +highlighter = require '../utils/highlighter' +Rx = require 'rx-lite' +{ fontOptions } = require '../utils/dom' + +styles = fontOptions() + +# highlight : forall a. +# { code : String, highlightInformation : HighlightInformation } -> +# CycleDOM +highlight = ({ code, highlightInformation }) -> + if highlightInformation + highlights = highlighter.highlight code, highlightInformation + highlighter.highlightToCycle highlights + else + code + +displaySuccess = (line) -> + highlightedCode = highlight line + + CycleDOM.h 'pre', + { + className: 'idris-repl-output' + style: styles + }, + [ + highlightedCode + ] + +displayError = (line) -> + highlightedCode = highlight line + + CycleDOM.h 'pre', { }, highlightedCode + +REPLCycle = + # view : Observable State -> Observable CycleDOM + view: (state$) -> + state$.map (lines) -> + lines = lines.map (line) -> + answer = + if line.type == 'success' + displaySuccess line + else + displayError line + + CycleDOM.h 'div', + { + className: 'idris-repl-line' + style: styles + }, + [ + CycleDOM.h 'div', { className: 'idris-repl-input' }, + [ + CycleDOM.h 'span', { className: 'idris-repl-input-prompt' }, '> ' + line.input + ] + answer + ] + + CycleDOM.h 'div', + { + className: 'idris-panel-view' + }, + [ + CycleDOM.h 'input', { type: 'text', className: 'native-key-bindings idris-repl-input-field' }, '' + CycleDOM.h 'div', { className: 'idris-repl-lines' }, lines + ] + + main: (responses) -> + input = responses.DOM.select('input').events('keydown') + .filter (ev) -> ev.keyCode == 13 + .map (ev) -> ev.target.value + .startWith '' + + DOM: REPLCycle.view responses.CONTENT + CONTENT: input + + driver: + (options) -> + DOM: CycleDOM.makeDOMDriver options.hostElement + CONTENT: (inp) -> + inp + .filter (line) -> line != '' + .flatMap (line) -> + escapedLine = line.replace(/"/g, '\\"') + # append a space to trick the formatter, so that it wont turn + # the input into a symbol + options.model.interpret "#{escapedLine} " + .map (e) -> + type: 'success' + input: line + code: e.msg[0] + highlightInformation: e.msg[1] + .catch (e) -> + Rx.Observable.just + type: 'error' + input: line + code: e.message + highlightInformation: e.highlightInformation + warnings: e.warnings + .scan ((acc, x) -> [x].concat acc), [] + .startWith [] + +class REPLView + constructor: (params) -> + hostElement = document.createElement 'div' + @[0] = hostElement + + model = params.controller.model + + drivers = + REPLCycle.driver + hostElement: hostElement + model: model + + Cycle.run REPLCycle.main, drivers + +module.exports = REPLView diff --git a/package.json b/package.json index 1f1d764..5806b24 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "atom-message-panel": ">=0.1.0", "bennu": "17.3.0", "nu-stream": "3.3.1", - "rx-lite": "2.5.2" + "rx-lite": "4.0.0", + "@cycle/core": "3.1.0", + "@cycle/dom": "5.3.0" } } diff --git a/styles/repl.less b/styles/repl.less new file mode 100644 index 0000000..ccfcf4a --- /dev/null +++ b/styles/repl.less @@ -0,0 +1,37 @@ +@import "ui-variables"; + +.idris-repl-input-field +{ + width: 100%; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} + +.idris-panel-view +{ + height: 100%; + + .idris-repl-lines + { + height: 100%; + overflow: scroll; + } +} + +.repl.idris +{ + font-size: 1.3em; + min-height: 150px; + background-color: @pane-item-background-color; +} + +.idris-repl-line +{ + margin: 10px; + + .idris-repl-input + { + margin-bottom: 10px; + } +}