├── .gitignore ├── styles ├── idris-panel.less ├── holes.less ├── highlight.atom-text-editor.less └── repl.less ├── .editorconfig ├── lib ├── utils │ ├── js.ts │ ├── symbol.ts │ ├── dom.ts │ ├── editor.ts │ ├── parse.ts │ ├── Logger.ts │ ├── ipkg.ts │ └── highlighter.ts ├── typings │ ├── nu-stream.d.ts │ ├── atom-message-panel.d.ts │ └── bennu.d.ts ├── views │ ├── information-view.ts │ ├── panel-view.ts │ ├── holes-view.ts │ ├── apropos-view.tsx │ └── repl-view.tsx ├── protocol │ ├── sexp-formatter.ts │ ├── ide-protocol.ts │ └── to-sexp.ts ├── language-idris.ts ├── idris-ide-mode.ts ├── idris-model.ts └── idris-controller.ts ├── settings └── language-idris.json ├── .vscode └── settings.json ├── grammars ├── language-idris.literate.cson ├── language-ipkg.cson └── language-idris.cson ├── .prettierrc ├── coffeelint.json ├── spec ├── idris-ide-mode-spec.coffee ├── highlighter-spec.coffee └── parse-spec.coffee ├── tsconfig.json ├── LICENSE.md ├── documentation ├── ipkg.md └── tutorial.md ├── menus └── language-idris.cson ├── keymaps └── language-idris.json ├── README.md ├── DEVELOPMENT.md ├── snippets └── snippets.cson ├── package.json └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | dist/ 5 | -------------------------------------------------------------------------------- /styles/idris-panel.less: -------------------------------------------------------------------------------- 1 | .idris-panel 2 | { 3 | pre 4 | { 5 | font-size: 13px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /styles/holes.less: -------------------------------------------------------------------------------- 1 | .idris-holes-view 2 | { 3 | .idris-hole 4 | { 5 | margin-bottom: 2em; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | -------------------------------------------------------------------------------- /lib/utils/js.ts: -------------------------------------------------------------------------------- 1 | // slow method to compare objects. 2 | export const objectEqual = (a: any, b: any) => 3 | JSON.stringify(a) === JSON.stringify(b) 4 | -------------------------------------------------------------------------------- /lib/typings/nu-stream.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'nu-stream' { 2 | export type Stream = { 3 | toArray: any 4 | } 5 | 6 | export const stream: Stream 7 | } 8 | -------------------------------------------------------------------------------- /settings/language-idris.json: -------------------------------------------------------------------------------- 1 | { 2 | ".source.idris": { 3 | "editor": { 4 | "commentStart": "-- ", 5 | "increaseIndentPattern": "(((=|\\bdo|\\bwhere|\\bthen|\\belse|\\bof)\\s*)|(\\bif(?!.*\\bthen\\b.*\\belse\\b.*).*))$" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.formatOnSave": true 4 | }, 5 | "[typescriptreact]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "[json]": { 9 | "editor.formatOnSave": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /grammars/language-idris.literate.cson: -------------------------------------------------------------------------------- 1 | name: 'Idris (Literate)' 2 | scopeName: 'source.idris.literate' 3 | fileTypes: ['lidr'] 4 | patterns: 5 | [ 6 | { 7 | 'match': '^(?!>)(.*)(?=\n)' 8 | 'name': 'comment.line.documentation.idris.literate' 9 | } 10 | { 11 | 'include': 'source.idris' 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": false, 5 | "jsxSingleQuote": false, 6 | "printWidth": 80, 7 | "quoteProps": "as-needed", 8 | "semi": false, 9 | "singleQuote": true, 10 | "tabWidth": 4, 11 | "trailingComma": "all", 12 | "useTabs": false 13 | } 14 | -------------------------------------------------------------------------------- /lib/typings/atom-message-panel.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'atom-message-panel' { 2 | type MessagePanelView = any 3 | export const MessagePanelView: MessagePanelView 4 | 5 | type PlainMessageView = any 6 | export const PlainMessageView: PlainMessageView 7 | 8 | type LineMessageView = any 9 | export const LineMessageView: LineMessageView 10 | } 11 | -------------------------------------------------------------------------------- /styles/highlight.atom-text-editor.less: -------------------------------------------------------------------------------- 1 | @import "syntax-variables"; 2 | 3 | @red-color: @syntax-color-removed; 4 | 5 | atom-text-editor.editor 6 | { 7 | .gutter .line-number 8 | { 9 | &.highlight-idris-error 10 | { 11 | background-color: @red-color; 12 | } 13 | } 14 | .line 15 | { 16 | &.highlight-idris-error 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentation": 3 | { 4 | "value": 2, 5 | "level": "error" 6 | }, 7 | "braces_spacing": 8 | { 9 | "spaces": 1, 10 | "empty_object_spaces": 1, 11 | "level": "warn" 12 | }, 13 | "max_line_length": 14 | { 15 | "value": 120, 16 | "level": "warn" 17 | }, 18 | "space_operators": 19 | { 20 | "value": true, 21 | "level": "error" 22 | }, 23 | "spacing_after_comma": 24 | { 25 | "value": true, 26 | "level": "error" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/utils/symbol.ts: -------------------------------------------------------------------------------- 1 | export const operatorRegex = new RegExp(`(\ 2 | \\?[-!#\\$%&\\*\\+\\.\\/<=>@\\\\\\^\\|~:]+\ 3 | |[-!#\\$%&\\*\\+\\.\\/<=>@\\\\\\^\\|~:][-!#\\$%&\\*\\+\\.\\/<=>@\\\\\\^\\|~:\\?]*\ 4 | )`) 5 | 6 | export const isOperator = (chars: string): boolean => 7 | !!chars.match(operatorRegex) 8 | 9 | // puts parenthesis around a word if it's an operator 10 | export const serializeWord = (word: string): string => { 11 | if (isOperator(word)) { 12 | return `(${word})` 13 | } else { 14 | return word 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /styles/repl.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | .idris-repl-input-field 4 | { 5 | width: 100%; 6 | padding-left: 10px; 7 | padding-top: 5px; 8 | padding-bottom: 5px; 9 | } 10 | 11 | .idris-panel-view 12 | { 13 | height: 100%; 14 | 15 | .idris-repl-lines 16 | { 17 | height: 100%; 18 | overflow: scroll; 19 | } 20 | } 21 | 22 | .repl.idris 23 | { 24 | font-size: 1.3em; 25 | min-height: 150px; 26 | background-color: @pane-item-background-color; 27 | } 28 | 29 | .idris-repl-line 30 | { 31 | margin: 10px; 32 | 33 | .idris-repl-input 34 | { 35 | margin-bottom: 10px; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spec/idris-ide-mode-spec.coffee: -------------------------------------------------------------------------------- 1 | { IdrisIdeMode } = require "../lib/idris-ide-mode" 2 | child_process = require "child_process" 3 | { EventEmitter } = require "events" 4 | 5 | describe "Idris IDE mode", -> 6 | it "should not crash after ending the Idris process", -> 7 | mockProcess = new EventEmitter() 8 | mockProcess.stdout = new EventEmitter() 9 | mockProcess.stdout.setEncoding = -> mockProcess.stdout 10 | mockProcess.kill = -> 11 | # Idris prints this when it's killed 12 | # .emit calls the listeners synchronously 13 | mockProcess.stdout.emit('data', 'Alas the file is done, aborting') 14 | mockedSpawn = spyOn(child_process, 'spawn').andReturn mockProcess 15 | ideMode = new IdrisIdeMode() 16 | ideMode.start({ pkgs: [] }) 17 | ideMode.stop() 18 | -------------------------------------------------------------------------------- /grammars/language-ipkg.cson: -------------------------------------------------------------------------------- 1 | name: 'Idris Ipkg' 2 | scopeName: 'source.ipkg' 3 | fileTypes: ['ipkg'] 4 | patterns: 5 | [ 6 | { 7 | name: 'comment.line.ipkg' 8 | match: '(--).*$\n?' 9 | comment: 'Line comment' 10 | } 11 | { 12 | name: 'comment.block.ipkg' 13 | begin: '\\{-' 14 | end: '-\\}' 15 | comment: 'Block comment' 16 | } 17 | { 18 | name: 'keyword.control.ipkg' 19 | match: '\\b(package|opts|modules|sourcedir|makefile|objs|executable|main|libs|pkgs|tests)\\b' 20 | } 21 | { 22 | name: 'constant.language.ipkg', 23 | match: '\\b(brief|version|readme|license|author|maintainer|homepage|sourceloc|bugtracker)\\b' 24 | } 25 | { 26 | name: 'string.quoted.double.ipkg' 27 | begin: '"' 28 | end: '"' 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "importHelpers": true, 5 | "jsx": "react", 6 | "jsxFactory": "Preact.h", 7 | "lib": ["es7", "dom", "ES2017.object"], 8 | "module": "commonjs", 9 | "noLib": false, 10 | "outDir": "../dist", 11 | "preserveConstEnums": true, 12 | "skipLibCheck": false, 13 | "sourceMap": true, 14 | "strict": true, 15 | "target": "es2017", 16 | "noUnusedParameters": true, 17 | "noUnusedLocals": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noImplicitAny": true, 20 | "noImplicitThis": true, 21 | "plugins": [{ "name": "typescript-tslint-plugin" }], 22 | "newLine": "LF" 23 | }, 24 | "compileOnSave": true 25 | } 26 | -------------------------------------------------------------------------------- /lib/typings/bennu.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'bennu' { 2 | export type Parser = { 3 | map: any 4 | } 5 | 6 | export type Parse = { 7 | always: (t: T) => Parser 8 | attempt: any 9 | between: any 10 | choice: (...choices: Array>) => Parser 11 | either: (l: Parser, r: Parser) => Parser 12 | many: any 13 | many1: any 14 | next: (p1: Parser, p2: Parser) => Parser 15 | rec: any 16 | run: any 17 | token: (test: (c: string) => boolean) => any 18 | } 19 | 20 | export const parse: Parse 21 | 22 | export type Text = { 23 | character: (c: string) => Parser 24 | digit: Parser 25 | noneOf: any 26 | run: any 27 | space: Parser 28 | string: (s: string) => Parser 29 | } 30 | 31 | export const text: Text 32 | 33 | export type Lang = { 34 | between: any 35 | sepBy: any 36 | } 37 | 38 | export const lang: Lang 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Morten Fangel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/views/information-view.ts: -------------------------------------------------------------------------------- 1 | import * as highlighter from '../utils/highlighter' 2 | import * as dom from '../utils/dom' 3 | 4 | export class InformationViewClass extends HTMLElement { 5 | obligation: any 6 | highlightingInfo: any 7 | text: any 8 | 9 | initialize(params: any) { 10 | this.classList.add('idris-panel') 11 | this.obligation = params.obligation 12 | this.highlightingInfo = params.highlightingInfo 13 | if (this.highlightingInfo != null) { 14 | const highlighting = highlighter.highlight( 15 | this.obligation, 16 | this.highlightingInfo, 17 | ) 18 | const info = highlighter.highlightToHtml(highlighting) 19 | const pre = dom.createCodeElement() 20 | pre.appendChild(info) 21 | return this.appendChild(pre) 22 | } else { 23 | return this.text(this.obligation) 24 | } 25 | } 26 | } 27 | 28 | export const InformationView = (document as any).registerElement( 29 | 'idris-informations-view', 30 | { prototype: InformationViewClass.prototype }, 31 | ) 32 | -------------------------------------------------------------------------------- /documentation/ipkg.md: -------------------------------------------------------------------------------- 1 | # IPKG files 2 | 3 | When you open a directory with a file at the top level in it that ends in `.ipkg`, all the commands 4 | in this package will read it and use it to find the path of your sources and resolve 5 | dependencies. 6 | 7 | Supported are the `opts` and `sourcedir` options. 8 | 9 | There is [more information](http://docs.idris-lang.org/en/latest/tutorial/packages.html) about `ipkg`-files in the idris documentation. 10 | 11 | 12 | ## Example 13 | 14 | You have a folder that looks like this: 15 | 16 | ``` 17 | src 18 | └───Main.idr 19 | └───OtherFile.idr 20 | your-project.ipkg 21 | ``` 22 | 23 | with `your-project.ipkg` containing: 24 | 25 | ``` 26 | package yourProject 27 | 28 | sourcedir = src 29 | modules = Main 30 | executable = yourExecutable 31 | main = Main 32 | 33 | opts = "-p lightyear effects" 34 | ``` 35 | 36 | the package will search in the `src`-directory for your files and will 37 | load the dependencies specified in `opts`. 38 | 39 | Newer versions of Idris also accept packages specified in a comma-separated list 40 | under the `pkgs` key: 41 | 42 | ``` 43 | package yourProject 44 | 45 | sourcedir = src 46 | modules = Main 47 | executable = yourExecutable 48 | main = Main 49 | 50 | pkgs = lightyear, effects 51 | ``` 52 | -------------------------------------------------------------------------------- /lib/protocol/sexp-formatter.ts: -------------------------------------------------------------------------------- 1 | import { SExp } from './ide-protocol' 2 | 3 | /* 4 | * Takes an s-expression and formats it for sending. 5 | * It serializes it, adds a newline at the end and 6 | * prepends it with the length of the command. 7 | */ 8 | export const serialize = function (sexp: SExp) { 9 | const msg = formatSexp(sexp) + '\n' 10 | return `${hexLength(msg)}${msg}` 11 | } 12 | 13 | /** 14 | * Returns a 0-padded 6-char long hexadecimal 15 | * for the length of the input `str` 16 | */ 17 | export const hexLength = function (str: string) { 18 | const hex = str.length.toString(16) 19 | return Array(7 - hex.length).join('0') + hex 20 | } 21 | 22 | /** 23 | * Serializes an s-expression. 24 | */ 25 | export const formatSexp = (sexp: SExp): string => { 26 | switch (sexp.type) { 27 | case 'list': { 28 | return `(${sexp.data.map(formatSexp).join(' ')})` 29 | } 30 | case 'string': { 31 | return `"${sexp.data.trim()}"` 32 | } 33 | case 'bool': { 34 | return sexp.data ? ':True' : ':False' 35 | } 36 | case 'integer': { 37 | return `${sexp.data}` 38 | } 39 | case 'symbol': { 40 | return `:${sexp.data.trim()}` 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/language-idris.ts: -------------------------------------------------------------------------------- 1 | import { IdrisController } from './idris-controller' 2 | import { CompositeDisposable } from 'atom' 3 | import * as url from 'url' 4 | import { IdrisPanel } from './views/panel-view' 5 | 6 | let controller: IdrisController | null = null 7 | let subscriptions = new CompositeDisposable() 8 | 9 | export function activate() { 10 | controller = new IdrisController() 11 | 12 | const subscription = atom.commands.add( 13 | 'atom-text-editor[data-grammar~="idris"]', 14 | controller.getCommands(), 15 | ) 16 | subscriptions = new CompositeDisposable() 17 | subscriptions.add(subscription) 18 | 19 | atom.workspace.addOpener((uriToOpen: string) => { 20 | try { 21 | const { protocol, host } = url.parse(uriToOpen) 22 | if (protocol === 'idris:' && controller) { 23 | return new IdrisPanel(controller, host || '') 24 | } 25 | } catch (error) { 26 | return 27 | } 28 | }) 29 | } 30 | 31 | export const deactivate = () => { 32 | subscriptions.dispose() 33 | if (controller) { 34 | controller.destroy() 35 | } 36 | } 37 | 38 | export const provide = () => { 39 | if (controller) { 40 | return controller.provideReplCompletions() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/views/panel-view.ts: -------------------------------------------------------------------------------- 1 | import { REPLView } from './repl-view' 2 | import { AproposView } from './apropos-view' 3 | import { IdrisController } from '../idris-controller' 4 | 5 | export class IdrisPanel { 6 | panel: string = '' 7 | controller: IdrisController 8 | 9 | constructor(controller: IdrisController, panel: string) { 10 | this.controller = controller 11 | this.panel = panel 12 | } 13 | 14 | getTitle() { 15 | switch (this.panel) { 16 | case 'repl': { 17 | return 'Idris: REPL' 18 | } 19 | case 'apropos': { 20 | return 'Idris: Apropos' 21 | } 22 | default: { 23 | return 'Idris ?' 24 | } 25 | } 26 | } 27 | 28 | getViewClass() { 29 | switch (this.panel) { 30 | case 'repl': { 31 | return REPLView 32 | } 33 | case 'apropos': { 34 | return AproposView 35 | } 36 | default: 37 | return undefined 38 | } 39 | } 40 | 41 | getURI() { 42 | switch (this.panel) { 43 | case 'repl': { 44 | return 'idris://repl' 45 | } 46 | case 'apropos': { 47 | return 'idris://apropos' 48 | } 49 | default: 50 | return undefined 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /menus/language-idris.cson: -------------------------------------------------------------------------------- 1 | 'menu': 2 | [ 3 | { 4 | 'label': 'Packages' 5 | 'submenu': 6 | [ 7 | 'label': 'Idris' 8 | 'submenu': 9 | [ 10 | { 11 | 'label': 'Typecheck' 12 | 'command': 'language-idris:typecheck' 13 | } 14 | { 15 | 'label': 'Open REPL' 16 | 'command': 'language-idris:open-repl' 17 | } 18 | { 19 | 'label': 'Apropos' 20 | 'command': 'language-idris:apropos' 21 | } 22 | { 23 | 'label': 'Holes' 24 | 'command': 'language-idris:holes' 25 | } 26 | { 27 | 'label': 'Stop Compiler' 28 | 'command': 'language-idris:stop-compiler' 29 | } 30 | ] 31 | ] 32 | } 33 | ] 34 | 'context-menu': 35 | 'atom-text-editor[data-grammar~=\"idris\"]': [ 36 | { 37 | 'label': 'Idris' 38 | 'submenu': [ 39 | { 40 | 'label': 'Typecheck' 41 | 'command': 'language-idris:typecheck' 42 | } 43 | { 44 | 'label': 'Type search' 45 | 'command': 'language-idris:type-of' 46 | } 47 | { 48 | 'label': 'Docs for' 49 | 'command': 'language-idris:docs-for' 50 | } 51 | ] 52 | } 53 | ] 54 | -------------------------------------------------------------------------------- /keymaps/language-idris.json: -------------------------------------------------------------------------------- 1 | { 2 | "atom-text-editor[data-grammar~=\"idris\"]": { 3 | "ctrl-alt-c": "language-idris:case-split", 4 | "ctrl-alt-t": "language-idris:type-of", 5 | "ctrl-alt-a": "language-idris:add-clause", 6 | "ctrl-alt-s": "language-idris:proof-search", 7 | "ctrl-alt-d": "language-idris:docs-for", 8 | "ctrl-alt-enter": "language-idris:open-repl", 9 | "ctrl-alt-w": "language-idris:make-with", 10 | "ctrl-alt-l": "language-idris:make-lemma", 11 | "ctrl-alt-r": "language-idris:typecheck", 12 | "ctrl-alt-m": "language-idris:make-case", 13 | "ctrl-alt-p": "language-idris:add-proof-clause", 14 | "ctrl-alt-b": "language-idris:browse-namespace", 15 | "escape": "language-idris:close-information-view" 16 | }, 17 | 18 | ".platform-darwin, atom-text-editor[data-grammar~=\"idris\"]": { 19 | "ctrl-cmd-c": "language-idris:case-split", 20 | "ctrl-cmd-t": "language-idris:type-of", 21 | "ctrl-cmd-a": "language-idris:add-clause", 22 | "ctrl-cmd-s": "language-idris:proof-search", 23 | "ctrl-cmd-d": "language-idris:docs-for", 24 | "ctrl-cmd-enter": "language-idris:open-repl", 25 | "ctrl-cmd-w": "language-idris:make-with", 26 | "ctrl-cmd-l": "language-idris:make-lemma", 27 | "ctrl-cmd-r": "language-idris:typecheck", 28 | "ctrl-cmd-m": "language-idris:make-case", 29 | "ctrl-cmd-p": "language-idris:add-proof-clause", 30 | "ctrl-cmd-b": "language-idris:browse-namespace", 31 | "escape": "language-idris:close-information-view" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/utils/dom.ts: -------------------------------------------------------------------------------- 1 | export const joinHtmlElements = function (containerElem: any, elems: any) { 2 | const div = document.createElement(containerElem) 3 | elems.forEach((elem: any) => div.appendChild(elem)) 4 | return div 5 | } 6 | 7 | export const createCodeElement = (): HTMLPreElement => { 8 | const pre = document.createElement('pre') 9 | const fontFamily = atom.config.get('language-idris.panelFontFamily') 10 | if (fontFamily !== '') { 11 | pre.style.fontFamily = fontFamily 12 | } 13 | const fontSize = atom.config.get('language-idris.panelFontSize') 14 | pre.style.fontSize = `${fontSize}px` 15 | const enableLigatures = atom.config.get('language-idris.panelFontLigatures') 16 | if (enableLigatures) { 17 | pre.style.fontFeatureSettings = '"liga"' 18 | } 19 | return pre 20 | } 21 | 22 | export const fontOptions = () => { 23 | const fontSize = atom.config.get('language-idris.panelFontSize') 24 | const fontSizeAttr = `${fontSize}px` 25 | const enableLigatures = atom.config.get('language-idris.panelFontLigatures') 26 | const webkitFontFeatureSettings = enableLigatures ? '"liga"' : '"inherit"' 27 | 28 | const fontFamily = atom.config.get('language-idris.panelFontFamily') 29 | if (fontFamily !== '') { 30 | fontFamily 31 | } else { 32 | '"inherit"' 33 | } 34 | 35 | return { 36 | 'font-size': fontSizeAttr, 37 | '-webkit-font-feature-settings': webkitFontFeatureSettings, 38 | 'font-family': fontFamily, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/utils/editor.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor } from 'atom' 2 | 3 | export const isCurrentLineEmpty = (editor: TextEditor): boolean => { 4 | // save the current buffer range, so that we can 5 | // reset the state in the end 6 | const bufferRange = editor.getSelectedBufferRange() 7 | 8 | editor.moveToBeginningOfLine() 9 | editor.selectToEndOfLine() 10 | const selectedText = editor.getSelectedText() 11 | 12 | // reset the selection to what it was before calling 13 | // this function 14 | editor.setSelectedBufferRange(bufferRange) 15 | 16 | return selectedText.trim() === '' 17 | } 18 | 19 | const isCurrentLineLastOfFile = (editor: TextEditor): boolean => { 20 | const currentRow = editor.getCursorBufferPosition().row 21 | const totalRows = editor.getLineCount() 22 | return currentRow === totalRows - 1 23 | } 24 | 25 | export const moveToNextEmptyLine = (editor: TextEditor): void => { 26 | while (!isCurrentLineEmpty(editor) && !isCurrentLineLastOfFile(editor)) { 27 | editor.moveDown() 28 | } 29 | 30 | if (!isCurrentLineEmpty(editor)) { 31 | editor.insertNewlineBelow() 32 | } 33 | 34 | editor.moveToBeginningOfLine() 35 | } 36 | 37 | // the REGEXP to define what constitutes a word 38 | const options = { 39 | wordRegex: /(^[ ]*$|[^\s\/\\\(\)":,\.;<>~!@#\$%\^&\*\|\+=\[\]\{\}`\?\-…]+)|(\?[-!#\$%&\*\+\.\/<=>@\\\^\|~:]+|[-!#\$%&\*\+\.\/<=>@\\\^\|~:][-!#\$%&\*\+\.\/<=>@\\\^\|~:\?]*)+/g, 40 | } 41 | 42 | // get the word or operator under the cursor 43 | export const getWordUnderCursor = (editor: TextEditor): string => { 44 | const range = editor.getLastCursor().getCurrentWordBufferRange(options) 45 | return editor.getTextInBufferRange(range) 46 | } 47 | -------------------------------------------------------------------------------- /lib/utils/parse.ts: -------------------------------------------------------------------------------- 1 | import { parse, text, lang } from 'bennu' 2 | import { stream } from 'nu-stream' 3 | 4 | // this module defines the parse required to deal with S-Expressions 5 | // as used for the communication with the Idris IDE 6 | 7 | const streamToString = (s: any): string => stream.toArray(s).join('') 8 | 9 | // bool 10 | export const trueP = parse.next(text.string(':True'), parse.always(true)) 11 | export const falseP = parse.next(text.string(':False'), parse.always(false)) 12 | const boolP = parse.either(trueP, falseP) 13 | 14 | // integer 15 | export const integerP = parse 16 | .many1(text.digit) 17 | .map(streamToString) 18 | .map((s: string) => parseInt(s, 10)) 19 | 20 | // string 21 | const quoteP = text.character('"') 22 | const escapedP = parse.choice( 23 | parse.next(text.character('\\'), parse.always('\\')), 24 | parse.next(text.character('"'), parse.always('"')), 25 | ) 26 | const stringLetterP = parse.token((c: string) => c !== '"' && c !== '\\') 27 | const stringEscapeP = parse.attempt(parse.next(text.character('\\'), escapedP)) 28 | const stringBackslashP = text.character('\\') 29 | export const stringCharP = parse.choice( 30 | stringLetterP, 31 | stringEscapeP, 32 | stringBackslashP, 33 | ) 34 | export const stringP = lang 35 | .between(quoteP, quoteP, parse.many(stringCharP)) 36 | .map(streamToString) 37 | 38 | // symbol 39 | const symbolStartP = text.character(':') 40 | const symbolChar = text.noneOf(' )') 41 | export const symbolP = parse 42 | .next(symbolStartP, parse.many(symbolChar)) 43 | .map(streamToString) 44 | .map((symbol: string) => `:${symbol}`) 45 | 46 | // sexp 47 | const openP = text.character('(') 48 | const closeP = text.character(')') 49 | const sexpP = parse.rec((self: any) => { 50 | const choices = parse.choice(boolP, integerP, stringP, symbolP, self) 51 | return lang 52 | .between(openP, closeP, lang.sepBy(text.space, choices)) 53 | .map(stream.toArray) 54 | }) 55 | 56 | export const parseCommand = (input: string) => parse.run(sexpP, input) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # An Idris Mode for Atom 4 | 5 | A work-in-progress Idris Mode for Atom. 6 | 7 | It supports: 8 | 9 | - Typechecking (ctrl-alt-r) 10 | - compiles the file and reports errors 11 | - Case-splitting (ctrl-alt-c) 12 | - split a variable which can be pattern matched 13 | - Clause-adding (ctrl-alt-a) 14 | - add a clause to a function 15 | - Proof-search (ctrl-alt-s) 16 | - search for a proof of a hole 17 | - Showing the types of a variable (ctrl-alt-t) 18 | - show the type of a hole 19 | - Show the doc for a function (ctrl-alt-d) 20 | - make-with (ctrl-alt-w) 21 | - add further variables on the left hand side of a function 22 | - make-case (ctrl-alt-m) 23 | - make-lemma (ctrl-alt-l) 24 | - lift a hole into a function context 25 | - Add proof case (ctrl-alt-p) 26 | - alternate version of clause adding when trying to proof a type. http://docs.idris-lang.org/en/latest/reference/misc.html#match-application 27 | - Browse namespace (ctrl-alt-b) 28 | - select the name of a namespace beforehand 29 | - Showing holes 30 | - ipkg highlighting 31 | - REPL (ctrl-alt-enter) 32 | - Apropos view 33 | 34 | ## Usage 35 | 36 | The package should work after installation. The only thing you might need to 37 | set is the path to the `idris` executable in the config of this package. 38 | If it doesn't work it's probably a bug. 39 | 40 | There is a tutorial on how to use the editor under [`documentation/tutorial.md`](https://github.com/idris-hackers/atom-language-idris/blob/master/documentation/tutorial.md). 41 | 42 | ### Working with ipkg files 43 | 44 | Place your ipkg file in the top level directory of your project. 45 | There is more information available in a in a [separate documentation](https://github.com/idris-hackers/atom-language-idris/blob/master/documentation/ipkg.md). 46 | 47 | ## Todo 48 | 49 | - Add better support for drawing attention to error-messages 50 | - Improve the syntax-highlighting (the current is based on the Sublime plugin) 51 | - Add autocompletion 52 | - ... 53 | 54 | ## Development 55 | 56 | see the [Development Guide](DEVELOPMENT.md) 57 | -------------------------------------------------------------------------------- /lib/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | import { appendFile } from 'fs' 2 | import * as sexpFormatter from '../protocol/sexp-formatter' 3 | import { SExp } from '../protocol/ide-protocol' 4 | 5 | type LogOutput = 6 | | { type: 'none' } 7 | | { type: 'console' } 8 | | { type: 'file'; path: string } 9 | 10 | class Logger { 11 | logOutput: LogOutput = { type: 'none' } 12 | 13 | logText(str: string): void { 14 | switch (this.logOutput.type) { 15 | case 'none': { 16 | return 17 | } 18 | case 'console': { 19 | console.log(str) 20 | return 21 | } 22 | case 'file': { 23 | appendFile(this.logOutput.path, str, (err) => { 24 | if (err) { 25 | throw err 26 | } 27 | }) 28 | return 29 | } 30 | } 31 | } 32 | 33 | logObject(description: string, obj: object): void { 34 | switch (this.logOutput.type) { 35 | case 'none': { 36 | return 37 | } 38 | case 'console': { 39 | console.log(description, obj) 40 | return 41 | } 42 | case 'file': { 43 | const output = `=====\n${description}:\n\n${JSON.stringify( 44 | obj, 45 | undefined, 46 | 4, 47 | )}` 48 | appendFile(this.logOutput.path, output, (err) => { 49 | if (err) { 50 | throw err 51 | } 52 | }) 53 | return 54 | } 55 | } 56 | } 57 | 58 | formatCommand(cmd: SExp): string { 59 | return sexpFormatter.serialize(cmd) 60 | } 61 | 62 | logIncomingCommand(str: string): void { 63 | this.logText(`< ${str}\n`) 64 | } 65 | 66 | logOutgoingCommand(cmd: any): void { 67 | const str = this.formatCommand(cmd) 68 | this.logText(`> ${str}`) 69 | } 70 | } 71 | 72 | export default new Logger() 73 | -------------------------------------------------------------------------------- /spec/highlighter-spec.coffee: -------------------------------------------------------------------------------- 1 | highlighter = require '../lib/utils/highlighter' 2 | 3 | describe "The highlighter", -> 4 | it "should highlight correctly.", -> 5 | code1 = "Prelude.Nat.Nat : Type" 6 | info1 = 7 | [ 8 | [ 9 | 0, 10 | 15, 11 | [ 12 | [":name","Prelude.Nat.Nat"], 13 | [":implicit",false], 14 | [":decor",":type"], 15 | [":doc-overview","Unary natural numbers"], 16 | [":type","Type"], 17 | [":namespace","Prelude.Nat"] 18 | ] 19 | ], 20 | [ 21 | 18, 22 | 4, 23 | [ 24 | [":decor",":type"], 25 | [":type","Type"], 26 | [":doc-overview","The type of types"], 27 | [":name","Type"] 28 | ] 29 | ], 30 | [ 31 | 18, 32 | 4, 33 | [ 34 | [":tt-term","AAAAAAAAAAAHAP//////////"] 35 | ] 36 | ] 37 | ] 38 | should1 = 39 | [ 40 | { 41 | classes: ['syntax--storage', 'syntax--type', 'syntax--idris'] 42 | description: "Type\n\nUnary natural numbers" 43 | word: 'Prelude.Nat.Nat' 44 | } 45 | { 46 | classes: [] 47 | description: '' 48 | word: ' : ' 49 | } 50 | { 51 | classes: ['syntax--storage', 'syntax--type', 'syntax--idris'] 52 | description: "Type\n\nThe type of types" 53 | word: 'Type' 54 | } 55 | ] 56 | should1String = 'Prelude.Nat.Nat : Type' 57 | should1Html = 'Prelude.Nat.Nat : Type' 58 | highlight1 = highlighter.highlight(code1, info1) 59 | console.log highlight1 60 | expect(highlight1).toEqual(should1) 61 | expect(highlighter.highlightToString(highlight1)).toEqual(should1String) 62 | html1 = highlighter.highlightToHtml(highlight1) 63 | expect(html1.outerHTML).toEqual(should1Html) 64 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | ## Getting started 4 | 5 | The easiest way is to get the source of the `language-idris` package via the `apm`-tooling: 6 | 7 | ```bash 8 | apm dev language-idris 9 | ``` 10 | 11 | This will install the package in the folder `~/github/language-idris`. You will then be able to use the development version of `language-idris` by invoking atom from any directory containing some idris files using 12 | 13 | ```bash 14 | atom -d . 15 | ``` 16 | 17 | ## Development process 18 | 19 | Atom is basically a browser and when doing development it can be useful to open the dev console. 20 | 21 | Alt+Cmdi opens the developer console on Mac OS X. This enables you to see console logging output and exceptions. 22 | 23 | You can edit the sourcecode in another atom window: 24 | 25 | ``` 26 | $~/github/language-idris> atom . 27 | ``` 28 | 29 | Anytime you want to restart your project with the latest changes, you can just reload the window using `Window: Reload`. 30 | 31 | ## Code Structure 32 | 33 | ```bash 34 | ~/github/language-idris/lib (master %)$ tree 35 | . 36 | ├── idris-controller.coffee 37 | ├── idris-ide-mode.coffee 38 | ├── idris-model.coffee 39 | ├── language-idris.coffee 40 | ├── utils 41 | │   ├── Logger.coffee 42 | │   ├── dom.coffee 43 | │   ├── editor.coffee 44 | │   ├── highlighter.coffee 45 | │   ├── ipkg.coffee 46 | │   ├── js.coffee 47 | │   ├── parse.coffee 48 | │   ├── sexp-formatter.coffee 49 | │   └── symbol.coffee 50 | └── views 51 | ├── apropos-view.coffee 52 | ├── holes-view.coffee 53 | ├── information-view.coffee 54 | ├── panel-view.coffee 55 | └── repl-view.coffee 56 | ``` 57 | 58 | The best point to get started is to dig into `idris-controller.coffee`. Almost all defined commands talk to a spawned idris process using the [Idris IDE protocol](http://docs.idris-lang.org/en/latest/reference/ide-protocol.html). This protocol communicates with Idris via S-Exp Expressions. If you want to see this communication have a look at `utils/Logger.coffee`. 59 | 60 | In order to send a command to idris, you will probably need some information from the current editor context. There are plenty of examples in the code and a helper package in the `utils` section. Once you have a reply you will probably need to format it. This can be done via one of the `highlighters`. Again, this is something which occurs again and again in the code. 61 | 62 | 63 | ## Specs 64 | 65 | There are (too) few tests defined in the spec directory. You can execute them using `Window: Run package specs`. 66 | -------------------------------------------------------------------------------- /snippets/snippets.cson: -------------------------------------------------------------------------------- 1 | '.source.idris': 2 | 'Show implementation': 3 | 'prefix': 'Show' 4 | 'body': """ 5 | Show ${1:Type} where 6 | show s = ?holeShow 7 | """ 8 | 'Eq implementation': 9 | 'prefix': 'Eq' 10 | 'body': """ 11 | Eq ${1:Type} where 12 | a == b = ?holeEq 13 | """ 14 | 'Ord implementation': 15 | 'prefix': 'Ord' 16 | 'body': """ 17 | Ord ${1:Type} where 18 | compare a b = ?holeOrd 19 | """ 20 | 'Semigroup implementation': 21 | 'prefix': 'Semigroup' 22 | 'body': """ 23 | Semigroup ${1:Type} where 24 | a <+> b = ?holeSemigroup 25 | """ 26 | 'Monoid implementation': 27 | 'prefix': 'Monoid' 28 | 'body': """ 29 | Monoid ${1:Type} where 30 | neutral = ?holeMonoid 31 | """ 32 | 'Num implementation': 33 | 'prefix': 'Num' 34 | 'body': """ 35 | Num ${1:Type} where 36 | (+) a b = ?holePlus 37 | (*) a b = ?holeMult 38 | fromInteger i = ?holeFromInteger 39 | """ 40 | 'Functor implementation': 41 | 'prefix': 'Functor' 42 | 'body': """ 43 | Functor ${1:Type} where 44 | map f fa = ?holeFunctor 45 | """ 46 | 'Applicative implementation': 47 | 'prefix': 'Applicative' 48 | 'body': """ 49 | Applicative ${1:Type} where 50 | pure a = ?holePureApplicative 51 | 52 | f <*> fa = ?holeApplyApplicative 53 | """ 54 | 'Monad implementation': 55 | 'prefix': 'Monad' 56 | 'body': """ 57 | Monad ${1:Type} where 58 | fa >>= f = ?holeMonadBind 59 | """ 60 | 'Traversable implementation': 61 | 'prefix': 'Traversable' 62 | 'body': """ 63 | Traversable ${1:Type} where 64 | traverse = ?holeTraverse 65 | """ 66 | 'Foldable implementation': 67 | 'prefix': 'Foldable' 68 | 'body': """ 69 | Foldable ${1:Type} where 70 | foldr = ?holeFoldableFoldr 71 | """ 72 | 'DecEq implementation': 73 | 'prefix': 'DecEq' 74 | 'body': """ 75 | DecEq ${1:Type} where 76 | decEq t1 t2 = ?holeDecEq 77 | """ 78 | 'Uninhabited implementation': 79 | 'prefix': 'Uninhabited' 80 | 'body': """ 81 | Uninhabited (${1:TypeWhichIsUninhabited}) where 82 | uninhabited ${2:Proof} impossible 83 | """ 84 | 'Cast implementation': 85 | 'prefix': 'Cast' 86 | 'body': """ 87 | Cast ${1:FromType} ${2:ToType} where 88 | case from = ?holeTo 89 | """ 90 | 'Alternative implementation': 91 | 'prefix': 'Alternative' 92 | 'body': """ 93 | Alternative ${1:Type} where 94 | empty = ?holeAlternativeEmpty 95 | 96 | fa <|> fb = ?holeAlternative 97 | """ 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "language-idris", 3 | "main": "./lib/language-idris", 4 | "version": "0.6.0", 5 | "private": true, 6 | "description": "A plugin for developing with Idris", 7 | "repository": "https://github.com/idris-hackers/atom-language-idris", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "tsc -p ./lib" 11 | }, 12 | "engines": { 13 | "atom": ">=1.31.0" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Morten Fangel", 18 | "url": "http://sevengoslings.net" 19 | }, 20 | { 21 | "name": "Nicolas Gagliani", 22 | "url": "http://twitter.com/archaeron" 23 | }, 24 | { 25 | "name": "Heather", 26 | "url": "https://github.com/Heather" 27 | }, 28 | { 29 | "name": "David Christiansen", 30 | "url": "http://www.davidchristiansen.dk" 31 | }, 32 | { 33 | "name": "Edwin Brady", 34 | "url": "https://edwinb.wordpress.com/" 35 | } 36 | ], 37 | "configSchema": { 38 | "pathToIdris": { 39 | "title": "Idris Location", 40 | "type": "string", 41 | "default": "idris", 42 | "description": "Location of the Idris executable (e.g. /usr/local/bin/idris)" 43 | }, 44 | "panelFontFamily": { 45 | "type": "string", 46 | "default": "", 47 | "description": "The font family to use in the various idris panels" 48 | }, 49 | "panelFontSize": { 50 | "type": "number", 51 | "default": 13, 52 | "description": "The font size to use in the various idris panels" 53 | }, 54 | "panelFontLigatures": { 55 | "type": "boolean", 56 | "default": false, 57 | "description": "Enable ligatures in the various idris panels" 58 | } 59 | }, 60 | "providedServices": { 61 | "autocomplete.provider": { 62 | "versions": { 63 | "2.0.0": "provide" 64 | } 65 | } 66 | }, 67 | "consumedServices": {}, 68 | "atomTranspilers": [ 69 | { 70 | "transpiler": "atom-ts-transpiler", 71 | "glob": "{!(node_modules)/**/,}*.ts?(x)", 72 | "options": { 73 | "compilerOptions": {}, 74 | "cacheKeyFiles": [], 75 | "verbose": false 76 | } 77 | } 78 | ], 79 | "dependencies": { 80 | "atom-message-panel": "^1.3.0", 81 | "atom-ts-transpiler": "^1.5.2", 82 | "bennu": "17.3.0", 83 | "nu-stream": "3.3.1", 84 | "preact": "10.4.4", 85 | "rx-lite": "4.0.8", 86 | "tslib": "1.11.1", 87 | "typescript": "3.9.2" 88 | }, 89 | "devDependencies": { 90 | "@types/atom": "1.40.4", 91 | "@types/rx-lite": "4.0.6", 92 | "prettier": "2.0.5" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/views/holes-view.ts: -------------------------------------------------------------------------------- 1 | import * as highlighter from '../utils/highlighter' 2 | import { createCodeElement, joinHtmlElements } from '../utils/dom' 3 | 4 | const textNode = (text: string): Text => document.createTextNode(text) 5 | 6 | export class HolesViewClass extends HTMLElement { 7 | holesContainer: HTMLPreElement | undefined 8 | 9 | initialize(holes: any): void { 10 | this.classList.add('idris-panel') 11 | this.holesContainer = createCodeElement() 12 | this.holesContainer.classList.add('idris-mode') 13 | this.holesContainer.classList.add('block') 14 | this.holesContainer.classList.add('idris-holes-view') 15 | 16 | this.appendChild(this.holesContainer) 17 | this.showHoles(holes) 18 | } 19 | 20 | showHoles(holes: any): void { 21 | this.holesContainer?.appendChild(this.prettyprintHoles(holes)) 22 | } 23 | 24 | prettyprintHoles(holes: any) { 25 | const html = holes.map( 26 | ([name, premises, conclusion]: [string, any, any]) => { 27 | return this.prettyprintHole(name, premises, conclusion) 28 | }, 29 | ) 30 | return joinHtmlElements('div', html) 31 | } 32 | 33 | prettyprintHole(name: string, premises: any, conclusion: any) { 34 | const prettyPremises = this.prettyprintPremises(premises) 35 | const prettyConclusion: any = this.prettyprintConclusion( 36 | name, 37 | conclusion, 38 | ) 39 | 40 | const hole = joinHtmlElements( 41 | 'div', 42 | [textNode(`${name}`)].concat(prettyPremises, prettyConclusion), 43 | ) 44 | hole.classList.add('idris') 45 | hole.classList.add('idris-hole') 46 | return hole 47 | } 48 | 49 | prettyprintPremises(premises: any) { 50 | const html = premises.map( 51 | ([name, type, highlightInformation]: [string, any, any]) => { 52 | const highlight = highlighter.highlight( 53 | type, 54 | highlightInformation, 55 | ) 56 | type = highlighter.highlightToHtml(highlight) 57 | return joinHtmlElements( 58 | 'div', 59 | [textNode(` ${name} : `)].concat(type), 60 | ) 61 | }, 62 | ) 63 | return joinHtmlElements('div', html) 64 | } 65 | 66 | prettyprintConclusion(name: string, [type, highlightInformation]: any) { 67 | const highlight = highlighter.highlight(type, highlightInformation) 68 | const highlightedConclusion = highlighter.highlightToHtml(highlight) 69 | const dividerLength = `${name} : ${type}`.length 70 | var divider = '' 71 | for (var i = 0; i < dividerLength; i++) { 72 | divider += '-' 73 | } 74 | 75 | return [ 76 | textNode(divider), 77 | document.createElement('br'), 78 | textNode(`${name} : `), 79 | highlightedConclusion, 80 | ] 81 | } 82 | } 83 | 84 | export const HolesView = (document as any).registerElement('idris-holes-view', { 85 | prototype: HolesViewClass.prototype, 86 | }) 87 | -------------------------------------------------------------------------------- /lib/views/apropos-view.tsx: -------------------------------------------------------------------------------- 1 | import * as Preact from 'preact' 2 | import { useState, StateUpdater } from 'preact/hooks' 3 | import * as highlighter from '../utils/highlighter' 4 | import * as Rx from 'rx-lite' 5 | import { fontOptions } from '../utils/dom' 6 | import { IdrisController } from '../idris-controller' 7 | import { IdrisModel } from '../idris-model' 8 | import { HighlightInformation } from '../utils/highlighter' 9 | 10 | const styles = fontOptions() 11 | 12 | const ask = ( 13 | model: IdrisModel, 14 | question: string, 15 | setAnswer: StateUpdater>, 16 | ) => { 17 | model 18 | .apropos(question) 19 | .map((e: any): { 20 | code: string 21 | highlightInfo: Array<[number, number, Array]> 22 | } => ({ 23 | code: e.msg[0], 24 | highlightInfo: e.msg[1], 25 | })) 26 | .catch((e: any) => 27 | Rx.Observable.just({ 28 | code: e.message, 29 | highlightInfo: e.highlightInformation, 30 | }), 31 | ) 32 | .subscribe( 33 | ({ code, highlightInfo }) => { 34 | const answer = highlighter.highlight(code, highlightInfo) 35 | setAnswer(answer) 36 | }, 37 | (err) => 38 | setAnswer([ 39 | { word: err.message, classes: [], description: '' }, 40 | ]), 41 | ) 42 | } 43 | 44 | type AnswerProps = { highlightInfo: Array } 45 | 46 | const Answer: Preact.FunctionComponent = (props) => { 47 | const { highlightInfo } = props 48 | return ( 49 |
50 |             {highlighter.highlightToPreact(highlightInfo)}
51 |         
52 | ) 53 | } 54 | 55 | type AproposProps = { model: IdrisModel } 56 | 57 | const Apropos: Preact.FunctionComponent = (props) => { 58 | const { model } = props 59 | const [input, setInput] = useState('') 60 | const [answer, setAnswer] = useState>([]) 61 | 62 | return ( 63 |
64 | { 68 | setInput(e.currentTarget.value) 69 | }} 70 | onKeyPress={(e) => { 71 | if (e.keyCode === 13) { 72 | ask(model, input, setAnswer) 73 | } 74 | }} 75 | > 76 | {input} 77 | 78 |
79 | 80 |
81 |
82 | ) 83 | } 84 | 85 | export class AproposView { 86 | 0: HTMLDivElement = document.createElement('div') 87 | 88 | constructor(params: { controller: IdrisController }) { 89 | const hostElement = this[0] 90 | 91 | const { model } = params.controller 92 | 93 | Preact.render(, hostElement) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/utils/ipkg.ts: -------------------------------------------------------------------------------- 1 | import { Project } from 'atom' 2 | import * as path from 'path' 3 | import * as fs from 'fs' 4 | import * as Rx from 'rx-lite' 5 | import Logger from './Logger' 6 | 7 | type IpkgFile = { 8 | file: string 9 | directory: string 10 | path: string 11 | ext: string 12 | } 13 | 14 | export type CompilerOptions = { 15 | options?: string 16 | pkgs: Array 17 | src?: string 18 | } 19 | 20 | const optionsRegexp = /opts\s*=\s*\"([^\"]*)\"/ 21 | const sourcedirRegexp = /sourcedir\s*=\s*([a-zA-Z/0-9.]+)/ 22 | const pkgsRegexp = /pkgs\s*=\s*(([a-zA-Z/0-9., -_]+\s{0,1})*)/ 23 | 24 | // Find all ipkg-files in a directory and returns 25 | // an observable of an array of files 26 | export const findIpkgFile = ( 27 | project: Project, 28 | ): Rx.Observable> => { 29 | const directories = project.getDirectories() 30 | const directory = directories[0] 31 | let directoryPath: string 32 | if (directory) { 33 | Logger.logText('Project detected') 34 | directoryPath = directory.getPath() 35 | } else { 36 | Logger.logText('Single file detected') 37 | const editor: any = atom.workspace.getActivePaneItem() 38 | const file = editor?.buffer?.file 39 | directoryPath = file.getParent().path 40 | } 41 | 42 | const readDir = Rx.Observable.fromNodeCallback(fs.readdir) 43 | 44 | const r = readDir(directoryPath) 45 | return r.map((files: any) => 46 | files 47 | .map( 48 | (file: string): IpkgFile => ({ 49 | file, 50 | directory: directoryPath, 51 | path: path.join(directoryPath, file), 52 | ext: path.extname(file), 53 | }), 54 | ) 55 | .filter((file: IpkgFile) => file.ext === '.ipkg'), 56 | ) 57 | } 58 | 59 | const parseIpkgFile = (fileInfo: any) => ( 60 | fileContents: string, 61 | ): CompilerOptions => { 62 | const optionsMatches = fileContents.match(optionsRegexp) 63 | const sourcedirMatches = fileContents.match(sourcedirRegexp) 64 | const pkgsMatches = fileContents.match(pkgsRegexp) 65 | 66 | const options = optionsMatches ? optionsMatches[1] : undefined 67 | 68 | const pkgs = pkgsMatches 69 | ? pkgsMatches[1].split(',').map((s: string) => s.trim()) 70 | : [] 71 | 72 | const src = sourcedirMatches 73 | ? path.join(fileInfo.directory, sourcedirMatches[1]) 74 | : fileInfo.directory 75 | 76 | return { options, pkgs, src } 77 | } 78 | 79 | export const readIpkgFile = (ipkgFile: any): Rx.Observable => { 80 | const readFile: any = Rx.Observable.fromNodeCallback(fs.readFile) 81 | return readFile(ipkgFile.path, { encoding: 'utf8' }) 82 | } 83 | 84 | // Find the ipkg file in the top directory of the project and return 85 | // the compiler options in it. 86 | export const compilerOptions = (project: Project) => { 87 | const ipkgFilesObserver = findIpkgFile(project) as any 88 | return ipkgFilesObserver 89 | .flatMap((ipkgFiles: any) => { 90 | if (ipkgFiles.length) { 91 | const ipkgFile = ipkgFiles[0] 92 | return readIpkgFile(ipkgFile).map(parseIpkgFile(ipkgFile)) 93 | } else { 94 | return Rx.Observable.return({}) 95 | } 96 | }) 97 | .catch(() => Rx.Observable.return({})) 98 | } 99 | -------------------------------------------------------------------------------- /lib/views/repl-view.tsx: -------------------------------------------------------------------------------- 1 | import * as Preact from 'preact' 2 | import { useState, StateUpdater } from 'preact/hooks' 3 | import { IdrisModel } from '../idris-model' 4 | import { HighlightInformation } from '../utils/highlighter' 5 | import * as highlighter from '../utils/highlighter' 6 | import * as Rx from 'rx-lite' 7 | import { fontOptions } from '../utils/dom' 8 | 9 | const styles = fontOptions() 10 | 11 | type ReplLineSuccess = { 12 | type: 'success' 13 | question: string 14 | answer: Array 15 | } 16 | 17 | type ReplLineError = { 18 | type: 'error' 19 | question: string 20 | answer: Array 21 | } 22 | 23 | type ReplLine = ReplLineSuccess | ReplLineError 24 | 25 | const ask = ( 26 | model: IdrisModel, 27 | question: string, 28 | lines: Array, 29 | setLines: StateUpdater>, 30 | ) => { 31 | const escapedLine = question.replace(/"/g, '\\"') 32 | // append a space to trick the formatter, so that it wont turn 33 | // the input into a symbol 34 | model 35 | .interpret(`${escapedLine} `) 36 | .map( 37 | (e: any): ReplLine => ({ 38 | type: 'success', 39 | question, 40 | answer: highlighter.highlight(e.msg[0], e.msg[1]), 41 | }), 42 | ) 43 | .catch((e: any): any => { 44 | const errorAnswer: ReplLineError = { 45 | type: 'error', 46 | question, 47 | answer: highlighter.highlight( 48 | e.message, 49 | e.highlightInformation, 50 | ), 51 | } 52 | const ob = Rx.Observable.just(errorAnswer) 53 | return ob 54 | }) 55 | .subscribe( 56 | (answer: ReplLine) => { 57 | setLines(lines.concat([answer])) 58 | }, 59 | (err) => console.log(err), 60 | ) 61 | } 62 | 63 | const SuccessAnswer: Preact.FunctionComponent<{ 64 | answer: Array 65 | }> = (props) => { 66 | const { answer } = props 67 | return ( 68 |
 69 |             {highlighter.highlightToPreact(answer)}
 70 |         
71 | ) 72 | } 73 | 74 | const ErrorAnswer: Preact.FunctionComponent<{ 75 | answer: Array 76 | }> = (props) => { 77 | const { answer } = props 78 | return
{highlighter.highlightToPreact(answer)}
79 | } 80 | 81 | const Answer: Preact.FunctionComponent> = ( 82 | props, 83 | ) => { 84 | const { type, answer } = props 85 | switch (type) { 86 | case 'success': { 87 | return 88 | } 89 | case 'error': { 90 | return 91 | } 92 | } 93 | } 94 | 95 | const Line: Preact.FunctionComponent = (props) => { 96 | const { type, question, answer } = props 97 | return ( 98 |
99 |
100 | > {question} 101 |
102 | 103 |
104 | ) 105 | } 106 | 107 | type ReplProps = { model: IdrisModel } 108 | 109 | const Repl: Preact.FunctionComponent = (props) => { 110 | const { model } = props 111 | 112 | const [input, setInput] = useState('') 113 | const [lines, setLines] = useState>([]) 114 | 115 | return ( 116 |
117 | { 121 | setInput(e.currentTarget.value) 122 | }} 123 | onKeyPress={(e) => { 124 | if (e.keyCode === 13) { 125 | ask(model, input, lines, setLines) 126 | } 127 | }} 128 | > 129 |
130 | {lines.map((line, index) => ( 131 | 137 | ))} 138 |
139 |
140 | ) 141 | } 142 | 143 | export class REPLView { 144 | 0: HTMLDivElement = document.createElement('div') 145 | 146 | constructor(params: any) { 147 | const hostElement = this[0] 148 | 149 | const { model } = params.controller 150 | 151 | Preact.render(, hostElement) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /spec/parse-spec.coffee: -------------------------------------------------------------------------------- 1 | sexpFormatter = require '../lib/protocol/sexp-formatter' 2 | parse = require '../lib/utils/parse' 3 | runP = require('bennu').parse.run 4 | 5 | toSexp = (data) -> 6 | switch (typeof data) 7 | when 'object' 8 | if (data instanceof Array) 9 | {type: 'list', data: data.map(toSexp)} 10 | when 'string' 11 | if data[0] == ':' 12 | { 13 | type: 'symbol' 14 | data: data[1..] 15 | } 16 | else 17 | {type: 'string', data} 18 | when 'number' 19 | {type: 'integer', data} 20 | when 'boolean' 21 | {type: 'bool', data} 22 | 23 | test1 = "(:protocol-version 1 0)" 24 | list1 = [':protocol-version', 1, 0] 25 | 26 | test2 = "(:set-prompt \"*C:\\Programming\\Idris\\Start\\hello\" 1)" 27 | list2 = [":set-prompt", "*C:\\Programming\\Idris\\Start\\hello", 1] 28 | 29 | test3 = "(:return (:ok ()) 5)" 30 | list3 = [":return", [":ok", []], 5] 31 | 32 | test4 = """(:return (:ok "Main.a : Nat" ((0 6 ((:name "Main.a") (:implicit :False) (:decor :function) (:doc-overview "") (:type "Nat"))) (9 3 ((:name "Prelude.Nat.Nat") (:implicit :False) (:decor :type) (:doc-overview "Unary natural numbers") (:type "Type"))) (9 3 ((:tt-term "AAAAAAAAAAAAAwAAAAAACAAAAQyZWx1ZGU="))))) 2)""" 33 | list4 = 34 | [ 35 | ":return", 36 | [ 37 | ":ok", 38 | "Main.a : Nat", 39 | [ 40 | [ 41 | 0, 42 | 6, 43 | [ 44 | [":name", "Main.a"], 45 | [":implicit", false], 46 | [":decor", ":function"], 47 | [":doc-overview", ""], 48 | [":type", "Nat"] 49 | ] 50 | ], 51 | [ 52 | 9, 53 | 3, 54 | [ 55 | [":name", "Prelude.Nat.Nat"], 56 | [":implicit", false], 57 | [":decor", ":type"], 58 | [":doc-overview", "Unary natural numbers"] 59 | [":type", "Type"] 60 | ] 61 | ], 62 | [ 63 | 9, 64 | 3, 65 | [ 66 | [ 67 | ":tt-term", "AAAAAAAAAAAAAwAAAAAACAAAAQyZWx1ZGU=" 68 | ] 69 | ] 70 | ] 71 | ] 72 | ], 73 | 2 74 | ] 75 | 76 | test5 = """(:return (:ok "\\"Z\\" : String" ((0 3 ((:name "\\"Z\\""))))) 5)""" 77 | list5 = 78 | [ 79 | ":return" 80 | [ 81 | ":ok" 82 | '"Z" : String' 83 | [ 84 | [ 85 | 0 86 | 3 87 | [ 88 | [ 89 | ":name" 90 | '"Z"' 91 | ] 92 | ] 93 | ] 94 | ] 95 | ] 96 | 5 97 | ] 98 | 99 | test6 = """(:return (:ok "\\\\__pi_arg => \\\\__pi_arg1 => (__pi_arg1)") 6)""" 100 | list6 = 101 | [ 102 | ":return" 103 | [ 104 | ":ok" 105 | "\\__pi_arg => \\__pi_arg1 => (__pi_arg1)" 106 | ] 107 | 6 108 | ] 109 | 110 | test7 = "(:interpret \":cd C:/Path/to/dir\")" 111 | list7 = 112 | [ 113 | ":interpret" 114 | ":cd C:/Path/to/dir" 115 | ] 116 | sexp7 = 117 | { 118 | type: 'list' 119 | data: [ 120 | {type: 'symbol', data: "interpret"} 121 | {type: "string", data: ":cd C:/Path/to/dir"} 122 | ] 123 | } 124 | 125 | describe "The sub-parser(s)", -> 126 | it "for :True and :False should work.", -> 127 | expect(runP(parse.trueP, ':True')).toEqual(true) 128 | expect(runP(parse.falseP, ':False')).toEqual(false) 129 | 130 | it "for integers should work.", -> 131 | expect(runP(parse.integerP, '2345')).toEqual(2345) 132 | expect(runP(parse.integerP, '1')).toEqual(1) 133 | 134 | it "for symbols should work.", -> 135 | expect(runP(parse.symbolP, ':sym')).toEqual(':sym') 136 | 137 | it "for string chars should work.", -> 138 | expect(runP(parse.stringCharP, 'h')).toEqual('h') 139 | expect(runP(parse.stringCharP, '\\"')).toEqual('"') 140 | 141 | it "for strings should work.", -> 142 | expect(runP(parse.stringP, '"hello"')).toEqual('hello') 143 | expect(runP(parse.stringP, '"\\"Z\\""')).toEqual('"Z"') 144 | expect(runP(parse.stringP, '"\\"Z\\" : String"')).toEqual('"Z" : String') 145 | 146 | describe "A parser", -> 147 | it "should parse to the right list.", -> 148 | expect(parse.parseCommand(test1)).toEqual(list1) 149 | expect(parse.parseCommand(test2)).toEqual(list2) 150 | expect(parse.parseCommand(test3)).toEqual(list3) 151 | expect(parse.parseCommand(test4)).toEqual(list4) 152 | expect(parse.parseCommand(test5)).toEqual(list5) 153 | expect(parse.parseCommand(test6)).toEqual(list6) 154 | 155 | it "should serialize back again.", -> 156 | expect(sexpFormatter.formatSexp(toSexp(list1))).toEqual(test1) 157 | expect(sexpFormatter.formatSexp(toSexp(list2))).toEqual(test2) 158 | expect(sexpFormatter.formatSexp(toSexp(list3))).toEqual(test3) 159 | expect(sexpFormatter.formatSexp(toSexp(list4))).toEqual(test4) 160 | expect(sexpFormatter.formatSexp(sexp7)).toEqual(test7) 161 | 162 | it "should serialize common commands.", -> 163 | loadFile = toSexp [[':load-file', "idris.idr"], 1] 164 | expect(sexpFormatter.formatSexp(loadFile)).toEqual '((:load-file "idris.idr") 1)' 165 | -------------------------------------------------------------------------------- /lib/protocol/ide-protocol.ts: -------------------------------------------------------------------------------- 1 | export type SExpList = { type: 'list'; data: Array } 2 | export type StringAtom = { type: 'string'; data: string } 3 | export type BoolAtom = { type: 'bool'; data: boolean } 4 | export type IntegerAtom = { type: 'integer'; data: number } 5 | export type SymbolAtom = { type: 'symbol'; data: string } 6 | export type SExp = SExpList | StringAtom | BoolAtom | IntegerAtom | SymbolAtom 7 | 8 | /** 9 | * Interpret `code` at the Idris REPL, returning a highlighted result. 10 | */ 11 | export type InterpretCommand = { type: 'interpret'; code: string } 12 | 13 | /** 14 | * Load the named file. 15 | * If `lineNumber` is provided, the file is only loaded up to that line. Otherwise, the entire file is loaded. 16 | */ 17 | export type LoadFileCommand = { 18 | type: 'load-file' 19 | fileName: string 20 | lineNumber?: number 21 | } 22 | 23 | /** 24 | * Return the type of the name, written with Idris syntax in the `code`. 25 | * The reply may contain highlighting information. 26 | */ 27 | export type TypeOfCommand = { type: 'type-of'; code: string } 28 | 29 | type DocsForMode = 'overview' | 'full' 30 | /** Look up the documentation for NAME, and return it as a highlighted string. 31 | * If `mode` is `"overview"`, only the first paragraph of documentation is provided for `symbolName`. 32 | * If `mode` is `"full"`, or omitted, the full documentation is returned for `symbolName`. 33 | */ 34 | export type DocsForComand = { 35 | type: 'docs-for' 36 | symbolName: string 37 | mode: DocsForMode 38 | } 39 | 40 | /** 41 | * Generate a case-split for the pattern variable `symbolName` on program line `line`. 42 | * The pattern-match cases to be substituted are returned as a string with no highlighting. 43 | */ 44 | export type CaseSplitCommand = { 45 | type: 'case-split' 46 | line: number 47 | symbolName: string 48 | } 49 | 50 | /** 51 | * Generate an initial pattern-match clause for the function declared as `symbolName` on program line `line`. 52 | * The initial clause is returned as a string with no highlighting. 53 | */ 54 | export type AddClauseCommand = { 55 | type: 'add-clause' 56 | line: number 57 | symbolName: string 58 | } 59 | 60 | /** 61 | * Add a clause driven by the <== syntax. 62 | */ 63 | export type AddProofClauseCommand = { 64 | type: 'add-proof-clause' 65 | line: number 66 | symbolName: string 67 | } 68 | 69 | /** 70 | * Create a with-rule pattern match template for the clause of function `symbolName` on line `line`. 71 | * The new code is returned with no highlighting. 72 | */ 73 | export type MakeWithCommand = { 74 | type: 'make-with' 75 | line: number 76 | symbolName: string 77 | } 78 | 79 | /** 80 | * Create a top level function with a type which solves the hole named `symbolName` on line `line`. 81 | */ 82 | export type MakeLemmaCommand = { 83 | type: 'make-lemma' 84 | line: number 85 | symbolName: string 86 | } 87 | 88 | /** 89 | * Create a case pattern match template for the clause of function `symbolName` on line `line`. 90 | * The new code is returned with no highlighting. 91 | */ 92 | export type MakeCaseCommand = { 93 | type: 'make-case' 94 | line: number 95 | symbolName: string 96 | } 97 | 98 | /** 99 | * List the currently-active holes, with their types pretty-printed with `width` columns. 100 | */ 101 | export type MetavariablesComand = { 102 | type: 'metavariables' 103 | width: number 104 | } 105 | 106 | /** 107 | * Attempt to fill out the holes on `line` named `symbolName` by proof search. 108 | */ 109 | export type ProofSearchCommand = { 110 | type: 'proof-search' 111 | line: number 112 | symbolName: string 113 | } 114 | 115 | /** 116 | * Return the definition of `symbolName` as a highlighted string. 117 | */ 118 | export type PrintDefinitionCommand = { 119 | type: 'print-definition' 120 | symbolName: string 121 | } 122 | 123 | /** 124 | * Return the contents of `namespace`, like :browse at the command-line REPL. 125 | */ 126 | export type BrowseNamespaceCommand = { 127 | type: 'browse-namespace' 128 | namespace: string 129 | } 130 | 131 | /** 132 | * Search the documentation for mentions of `code`, and return any found as a list of highlighted strings. 133 | */ 134 | export type AproposCommand = { 135 | type: 'apropos' 136 | code: string 137 | } 138 | 139 | /** 140 | * Search names, types and documentations which contain `name`. 141 | * Return the result of tab-completing NAME as a REPL command. 142 | */ 143 | export type ReplCompletionsCommand = { 144 | type: 'repl-completions' 145 | name: string 146 | } 147 | 148 | /** 149 | * IDE commands we can send to Idris 150 | */ 151 | export type IDECommand = 152 | | InterpretCommand 153 | | LoadFileCommand 154 | | TypeOfCommand 155 | | DocsForComand 156 | | CaseSplitCommand 157 | | AddClauseCommand 158 | | AddProofClauseCommand 159 | | MakeWithCommand 160 | | MakeLemmaCommand 161 | | MakeCaseCommand 162 | | MetavariablesComand 163 | | ProofSearchCommand 164 | | PrintDefinitionCommand 165 | | BrowseNamespaceCommand 166 | | AproposCommand 167 | | ReplCompletionsCommand 168 | -------------------------------------------------------------------------------- /lib/utils/highlighter.ts: -------------------------------------------------------------------------------- 1 | import * as Preact from 'preact' 2 | import Logger from './Logger' 3 | 4 | export type HighlightInformation = { 5 | classes: Array 6 | word: string 7 | description: string 8 | } 9 | 10 | const highlightInfoListToOb = (list: Array) => { 11 | const obj: { [key: string]: any } = {} 12 | for (let x of list) { 13 | const key = x[0].slice(1) 14 | const value = x[1] 15 | obj[key] = value 16 | } 17 | return obj 18 | } 19 | 20 | // Use the right CSS classes, so that we can use the 21 | // syntax highlighting built into atom. 22 | const decorToClasses = (decor: string) => { 23 | switch (decor) { 24 | case ':type': 25 | return ['syntax--storage', 'syntax--type'] 26 | case ':function': 27 | return ['syntax--entity', 'syntax--name', 'syntax--function'] 28 | case ':data': 29 | return ['syntax--constant'] 30 | case ':keyword': 31 | return ['syntax--keyword'] 32 | case ':bound': 33 | return ['syntax--support', 'syntax--function'] 34 | case ':metavar': 35 | return ['syntax--constant'] 36 | default: 37 | Logger.logText('unknown decor: ' + decor) 38 | Logger.logText( 39 | 'you may want to review the highlighter.coffee script', 40 | ) 41 | return [] 42 | } 43 | } 44 | 45 | const highlightWord = (word: string, info: any): HighlightInformation => { 46 | const type = info.info.type || '' 47 | const doc = info.info['doc-overview'] || '' 48 | 49 | const description = info.info.type != null ? `${type}\n\n${doc}`.trim() : '' 50 | 51 | return { 52 | classes: decorToClasses(info.info.decor).concat('syntax--idris'), 53 | word, 54 | description, 55 | } 56 | } 57 | 58 | // Build highlighting information that we can then pass to one 59 | // of our serializers. 60 | export const highlight = ( 61 | code: string, 62 | highlightingInfo: Array<[number, number, Array]>, 63 | ): Array => { 64 | const highlighted = highlightingInfo 65 | .map(function ([start, length, info]) { 66 | return { 67 | start, 68 | length, 69 | info: highlightInfoListToOb(info), 70 | } 71 | }) 72 | .filter((i) => i.info.decor != null) 73 | .reduce<[number, Array]>( 74 | ([position, text], info) => { 75 | const newPosition = info.start + info.length 76 | const unhighlightedText: HighlightInformation = { 77 | classes: [], 78 | word: code.slice(position, info.start), 79 | description: '', 80 | } 81 | const highlightedWord = highlightWord( 82 | code.slice(info.start, newPosition), 83 | info, 84 | ) 85 | const newText = text.concat(unhighlightedText, highlightedWord) 86 | 87 | return [newPosition, newText] 88 | }, 89 | [0, []], 90 | ) 91 | 92 | const [position, text] = highlighted 93 | const rest = { 94 | classes: [], 95 | word: code.slice(position), 96 | description: '', 97 | } 98 | const higlightedWords = text.concat(rest) 99 | return higlightedWords.filter( 100 | (higlightedWord: any) => higlightedWord.word !== '', 101 | ) 102 | } 103 | 104 | // Applies the highlighting and returns the result as an html-string. 105 | export const highlightToString = (highlights: Array) => 106 | highlights 107 | .map(function ({ classes, word }) { 108 | if (classes.length === 0) { 109 | return word 110 | } else { 111 | return `${word}` 112 | } 113 | }) 114 | .join('') 115 | 116 | // Applies the highlighting and returns the result as a DOM-objects. 117 | export const highlightToHtml = (highlights: Array) => { 118 | const spans = highlights.map(function ({ classes, word }) { 119 | if (classes.length === 0) { 120 | return document.createTextNode(word) 121 | } else { 122 | const span = document.createElement('span') 123 | classes.forEach((c: any) => span.classList.add(c)) 124 | span.textContent = word 125 | return span 126 | } 127 | }) 128 | const container = document.createElement('span') 129 | spans.forEach((span: any) => container.appendChild(span)) 130 | return container 131 | } 132 | 133 | export const highlightToPreact = ( 134 | highlights: Array, 135 | ): Preact.VNode => { 136 | const highlighted = highlights.map(({ classes, word, description }) => { 137 | if (classes.length === 0) { 138 | return word as string 139 | } else { 140 | return Preact.h( 141 | 'span', 142 | { className: classes.join(' '), title: description }, 143 | word, 144 | ) 145 | } 146 | }) 147 | return Preact.h('div', {}, highlighted) 148 | } 149 | -------------------------------------------------------------------------------- /lib/idris-ide-mode.ts: -------------------------------------------------------------------------------- 1 | import Logger from './utils/Logger' 2 | import * as sexpFormatter from './protocol/sexp-formatter' 3 | import * as parse from './utils/parse' 4 | import { EventEmitter } from 'events' 5 | import { spawn, ChildProcessWithoutNullStreams } from 'child_process' 6 | import { SExp } from './protocol/ide-protocol' 7 | import { CompilerOptions } from './utils/ipkg' 8 | 9 | export class IdrisIdeMode extends EventEmitter { 10 | process: ChildProcessWithoutNullStreams | null = null 11 | buffer = '' 12 | idrisBuffers = 0 13 | 14 | start(compilerOptions: CompilerOptions) { 15 | if (this.process == null || this.process.killed) { 16 | const pathToIdris: string = atom.config.get( 17 | 'language-idris.pathToIdris', 18 | ) 19 | const pkgs: Array = (() => { 20 | if (compilerOptions.pkgs && compilerOptions.pkgs.length) { 21 | const p = compilerOptions.pkgs.map((p) => ['-p', p]) as any 22 | return [].concat.apply([], p) 23 | } else { 24 | return [] 25 | } 26 | })() 27 | 28 | let ipkgOptions = compilerOptions.options 29 | ? compilerOptions.options.split(' ') 30 | : [] 31 | 32 | const tabLength = atom.config.get('editor.tabLength', { 33 | scope: ['source.idris'], 34 | }) 35 | const configParams = [ 36 | '--ide-mode', 37 | '--indent-with=' + tabLength, 38 | '--indent-clause=' + tabLength, 39 | ] 40 | 41 | const parameters = configParams.concat(pkgs, ipkgOptions) 42 | 43 | const options = compilerOptions.src 44 | ? { cwd: compilerOptions.src } 45 | : {} 46 | this.process = spawn(pathToIdris, parameters, options) 47 | this.process.on('error', this.error) 48 | this.process.on('exit', this.exited) 49 | this.process.on('close', this.exited) 50 | this.process.on('disconnect', this.exited) 51 | 52 | return this.process.stdout 53 | .setEncoding('utf8') 54 | .on('data', this.stdout.bind(this)) 55 | } 56 | } 57 | 58 | send(cmd: SExp): void { 59 | if (this.process) { 60 | Logger.logOutgoingCommand(cmd) 61 | const serializedCommand = sexpFormatter.serialize(cmd) 62 | console.log('serializedCommand', serializedCommand) 63 | this.process.stdin.write(serializedCommand) 64 | } else { 65 | Logger.logText('Could not send command to the idris compiler') 66 | Logger.logOutgoingCommand(cmd) 67 | } 68 | } 69 | 70 | stop() { 71 | this.process?.removeAllListeners() 72 | this.process?.stdout.removeAllListeners() 73 | return this.process != null ? this.process.kill() : undefined 74 | } 75 | 76 | error(error: any) { 77 | const e = 78 | error.code === 'ENOENT' 79 | ? { 80 | short: "Couldn't find idris executable", 81 | long: `Couldn't find idris executable at \"${error.path}\"`, 82 | } 83 | : { 84 | short: error.code, 85 | long: error.message, 86 | } 87 | 88 | return atom.notifications.addError(e.short, { detail: e.long }) 89 | } 90 | 91 | exited(code: number, signal: NodeJS.Signals) { 92 | let long, short 93 | if (signal === 'SIGTERM') { 94 | short = 'The idris compiler was closed' 95 | long = 'You stopped the compiler' 96 | return atom.notifications.addInfo(short, { detail: long }) 97 | } else { 98 | short = 'The idris compiler was closed or crashed' 99 | long = signal 100 | ? `It was closed with the signal: ${signal}` 101 | : `It (probably) crashed with the error code: ${code}` 102 | return atom.notifications.addError(short, { detail: long }) 103 | } 104 | } 105 | 106 | running(): boolean { 107 | return !!this.process && !this.process.killed 108 | } 109 | 110 | stdout(data: string): Array { 111 | this.buffer += data 112 | const result = [] 113 | while (this.buffer.length > 6) { 114 | this.buffer = this.buffer.trimLeft().replace(/\r\n/g, '\n') 115 | // We have 6 chars, which is the length of the command 116 | const len = parseInt(this.buffer.substr(0, 6), 16) 117 | if (this.buffer.length >= 6 + len) { 118 | // We also have the length of the command in the buffer, so 119 | // let's read in the command 120 | const cmd = this.buffer.substr(6, len).trim() 121 | Logger.logIncomingCommand(cmd) 122 | // Remove the length + command from the buffer 123 | this.buffer = this.buffer.substr(6 + len) 124 | // And then we can try to parse to command.. 125 | const obj = parse.parseCommand(cmd.trim()) 126 | result.push(this.emit('message', obj)) 127 | } else { 128 | // We didn't have the entire command, so let's break the 129 | // while-loop and wait for the next data-event 130 | break 131 | } 132 | } 133 | return result 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /documentation/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | ## First steps 4 | 5 | ## Learning Idris 6 | 7 | This is an overview of the atom package for Idris. If you are interested in learning Idris you can find the [official documentation](http://docs.idris-lang.org/en/latest/), and the [official Idris tutorial](http://docs.idris-lang.org/en/latest/tutorial/). 8 | 9 | ### Installation 10 | 11 | Install the `language-idris` package from the atom settings. 12 | The package might tell you that you need to set the path to the `idris` executable 13 | in the settings. 14 | 15 | Create a new file and call it `ops.idr`. 16 | Paste this code into your new file: 17 | 18 | ```idris 19 | module Ops 20 | 21 | ||| Add two natural numbers. 22 | add : Nat -> Nat -> Nat 23 | add Z y = y 24 | add (S k) y = S (add k y) 25 | 26 | ||| Multiply two natural numbers. 27 | mul : Nat -> Nat -> Nat 28 | mul Z y = Z 29 | mul (S k) y = add y (mul k y) 30 | ``` 31 | 32 | ### Typecheck 33 | 34 | Open the command palette (`ctrl-shift-p` on Win/Linux) and select `Language Idris: Typecheck`. (or use `ctrl-alt-r`) 35 | 36 | ### Type info 37 | 38 | Select an instance of the `add` function in your code and press `ctrl-alt-t` or use the command palette (`ctrl-shift-p` on Win/Linux) and search for "Language Idris: Type Of". A panel should open at the bottom of your window showing you the type of the `add` function, `Ops.add : Nat -> Nat -> Nat`. 39 | Now try the same thing with the `mul` function. 40 | 41 | ### Show documentation 42 | 43 | Another useful command is triggered by selecting a word and pressing `ctrl-alt-d` (or "Language Idris: Docs for" from the command palette). You can try it on `add`, `mul` or `Nat` for instance. 44 | 45 | ### REPL 46 | 47 | You can create a REPL window by pressing `ctrl-alt-enter`. Enter REPL commands at the top, just as if you were using the REPL command line interface. 48 | 49 | ### Idris command line options and library package dependencies 50 | 51 | Sometimes you may have dependendencies on Idris packages, for instance Lightyear for parsing or Pruvioj for advanced theorem proving. 52 | In Atom you can specify these dependencies using the project model, which simply means using Open Folder rather than Open File 53 | from the File menu. Atom will look for a .ipkg file in the folder and load any dependencies listed. More details are described in 54 | [Working with ipkg files](https://github.com/idris-hackers/atom-language-idris/blob/master/documentation/ipkg.md). 55 | 56 | ## Interactive proofs using Idris and Atom 57 | 58 | We'll try to prove that the addition of natural numbers is associative for the 59 | purpose of this tutorial. 60 | 61 | Create a new file, call it `proving.idr` and insert the following code into it. 62 | 63 | ```idris 64 | module Main 65 | 66 | plusAssoc : (l, c, r : Nat) -> l `plus` (c `plus` r) = (l `plus` c) `plus` r 67 | ``` 68 | 69 | Load the file into Idris by typechecking it by pressing `ctrl-alt-r`. Then press `ctrl-shift-p` and type "Language Idris: Holes". 70 | 71 | At the bottom of your window should open a small panel with all holes you'll have to prove. 72 | Here it should just show: 73 | ``` 74 | Main.plusAssoc 75 | l : Nat 76 | c : Nat 77 | r : Nat 78 | ------------------------------------------------------ 79 | Main.plusAssoc : plus l (plus c r) = plus (plus l c) r 80 | ``` 81 | where `l : Nat, c : Nat, r : Nat` are variables you can use to prove 82 | `Main.plusAssoc : plus l (plus c r) = plus (plus l c) r`. 83 | 84 | If you put your cursor over `plusAssoc` in the `proving.idr` file and execute the command "Language Idris: Add Clause" (`ctrl-alt-a`) a line wil be inserted by atom at the bottom of your file. 85 | 86 | Your file should now look like this: 87 | ```idris 88 | module Main 89 | 90 | plusAssoc : (l, c, r : Nat) -> l `plus` (c `plus` r) = (l `plus` c) `plus` r 91 | plusAssoc l c r = ?plusAssoc_rhs 92 | ``` 93 | 94 | If you select the `l` in `plusAssoc l c r = ?plusAssoc_rhs` and press `ctrl-alt-c` ("Language Idris: Case Split") it splits the `Nat` at `l` 95 | into it's two cases `Z` (zero) and `(S k)` (the successor of `k`). 96 | Rename `k` to `l` as we had it before, to show that it is the left value. 97 | 98 | Your file should now look like this: 99 | ```idris 100 | module Main 101 | 102 | plusAssoc : (l, c, r : Nat) -> l `plus` (c `plus` r) = (l `plus` c) `plus` r 103 | plusAssoc Z c r = ?plusAssoc_rhs_1 104 | plusAssoc (S l) c r = ?plusAssoc_rhs_2 105 | ``` 106 | 107 | After type checking the file again, open the holes view and it will show you both holes: 108 | 109 | ``` 110 | Main.plusAssoc_rhs_1 111 | c : Nat 112 | r : Nat 113 | ------------------------------------------ 114 | Main.plusAssoc_rhs_1 : plus c r = plus c r 115 | 116 | Main.plusAssoc_rhs_2 117 | l : Nat 118 | c : Nat 119 | r : Nat 120 | -------------------------------------------------------------------- 121 | Main.plusAssoc_rhs_2 : S (plus l (plus c r)) = S (plus (plus l c) r) 122 | ``` 123 | 124 | Now you can see, that you need to prove that `plus c r = plus c r` for `Main.plusAssoc_rhs_1`. Idris can insert the code automatically for us. Select `plusAssoc_rhs_1` and press `ctrl+alt+s` ("Language Idris: Proof Search") and Idris will insert `Refl` for you. 125 | 126 | Now the file looks like this: 127 | ```idris 128 | module Main 129 | 130 | plusAssoc : (l, c, r : Nat) -> l `plus` (c `plus` r) = (l `plus` c) `plus` r 131 | plusAssoc Z c r = Refl 132 | plusAssoc (S l) c r = ?plusAssoc_rhs_2 133 | ``` 134 | 135 | Only one hole is left now: 136 | 137 | ``` 138 | Main.plusAssoc_rhs_2 139 | l : Nat 140 | c : Nat 141 | r : Nat 142 | -------------------------------------------------------------------- 143 | Main.plusAssoc_rhs_2 : S (plus l (plus c r)) = S (plus (plus l c) r) 144 | ``` 145 | 146 | Now replace the line 147 | 148 | ```idris 149 | plusAssoc (S l) c r = ?plusAssoc_rhs_2 150 | ``` 151 | 152 | with 153 | 154 | ```idris 155 | plusAssoc (S l) c r = rewrite plusAssoc l c r in ?plusAssoc_rhs_2 156 | ``` 157 | 158 | and after type checking the holes view now shows us: 159 | 160 | ``` 161 | Main.plusAssoc_rhs_2 162 | l : Nat 163 | c : Nat 164 | r : Nat 165 | _rewrite_rule : plus (plus l c) r = plus l (plus c r) 166 | -------------------------------------------------------------------- 167 | Main.plusAssoc_rhs_2 : S (plus (plus l c) r) = S (plus (plus l c) r) 168 | ``` 169 | 170 | Now you need to prove that `S (plus (plus l c) r) = S (plus (plus l c) r)` and Idris can again do this for us. 171 | 172 | And you end with the file 173 | 174 | ```idris 175 | module Main 176 | 177 | plusAssoc : (l, c, r : Nat) -> l `plus` (c `plus` r) = (l `plus` c) `plus` r 178 | plusAssoc Z c r = Refl 179 | plusAssoc (S l) c r = rewrite plusAssoc l c r in Refl 180 | ``` 181 | 182 | and a proof that the addition of natural numbers is associative. 183 | 184 | This tutorial is a written version of [David Christiansen's](https://twitter.com/d_christiansen) emacs video for Atom. 185 | https://www.youtube.com/watch?v=0eOY1NxbZHo&list=PLiHLLF-foEexGJu1a0WH_llkQ2gOKqipg 186 | -------------------------------------------------------------------------------- /lib/protocol/to-sexp.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InterpretCommand, 3 | TypeOfCommand, 4 | SExp, 5 | IDECommand, 6 | LoadFileCommand, 7 | DocsForComand, 8 | SymbolAtom, 9 | StringAtom, 10 | CaseSplitCommand, 11 | AddClauseCommand, 12 | AddProofClauseCommand, 13 | MakeWithCommand, 14 | MakeLemmaCommand, 15 | MakeCaseCommand, 16 | MetavariablesComand, 17 | ProofSearchCommand, 18 | PrintDefinitionCommand, 19 | BrowseNamespaceCommand, 20 | AproposCommand, 21 | ReplCompletionsCommand, 22 | } from './ide-protocol' 23 | 24 | // See: https://github.com/edwinb/Idris2-boot/blob/master/src/Idris/IDEMode/Commands.idr 25 | 26 | const interpretCommandToSExp = (cmd: InterpretCommand): SExp => { 27 | return { 28 | type: 'list', 29 | data: [ 30 | { type: 'symbol', data: 'interpret' }, 31 | { type: 'string', data: cmd.code }, 32 | ], 33 | } 34 | } 35 | 36 | const loadFileCommandToSExp = (cmd: LoadFileCommand): SExp => { 37 | const commandSymbol: SymbolAtom = { type: 'symbol', data: 'load-file' } 38 | const fileName: StringAtom = { type: 'string', data: cmd.fileName } 39 | return { 40 | type: 'list', 41 | data: cmd.lineNumber 42 | ? [ 43 | commandSymbol, 44 | fileName, 45 | { type: 'integer', data: cmd.lineNumber }, 46 | ] 47 | : [commandSymbol, fileName], 48 | } 49 | } 50 | 51 | const typeOfCommandToSExp = (cmd: TypeOfCommand): SExp => { 52 | return { 53 | type: 'list', 54 | data: [ 55 | { type: 'symbol', data: 'type-of' }, 56 | { type: 'string', data: cmd.code }, 57 | ], 58 | } 59 | } 60 | 61 | const docsForComandToSExp = (cmd: DocsForComand): SExp => { 62 | return { 63 | type: 'list', 64 | data: [ 65 | { type: 'symbol', data: 'docs-for' }, 66 | { type: 'string', data: cmd.symbolName }, 67 | { type: 'symbol', data: cmd.mode }, 68 | ], 69 | } 70 | } 71 | 72 | const caseSplitCommandToSExp = (cmd: CaseSplitCommand): SExp => { 73 | return { 74 | type: 'list', 75 | data: [ 76 | { type: 'symbol', data: 'case-split' }, 77 | { type: 'integer', data: cmd.line }, 78 | { type: 'string', data: cmd.symbolName }, 79 | ], 80 | } 81 | } 82 | 83 | const addClauseCommandToSExp = (cmd: AddClauseCommand): SExp => { 84 | return { 85 | type: 'list', 86 | data: [ 87 | { type: 'symbol', data: 'add-clause' }, 88 | { type: 'integer', data: cmd.line }, 89 | { type: 'string', data: cmd.symbolName }, 90 | ], 91 | } 92 | } 93 | 94 | const addProofClauseCommandToSExp = (cmd: AddProofClauseCommand): SExp => { 95 | return { 96 | type: 'list', 97 | data: [ 98 | { type: 'symbol', data: 'add-proof-clause' }, 99 | { type: 'integer', data: cmd.line }, 100 | { type: 'string', data: cmd.symbolName }, 101 | ], 102 | } 103 | } 104 | 105 | const makeWithCommandToSExp = (cmd: MakeWithCommand): SExp => { 106 | return { 107 | type: 'list', 108 | data: [ 109 | { type: 'symbol', data: 'make-with' }, 110 | { type: 'integer', data: cmd.line }, 111 | { type: 'string', data: cmd.symbolName }, 112 | ], 113 | } 114 | } 115 | const makeLemmaCommandToSExp = (cmd: MakeLemmaCommand): SExp => { 116 | return { 117 | type: 'list', 118 | data: [ 119 | { type: 'symbol', data: 'make-lemma' }, 120 | { type: 'integer', data: cmd.line }, 121 | { type: 'string', data: cmd.symbolName }, 122 | ], 123 | } 124 | } 125 | 126 | const makeCaseCommandToSExp = (cmd: MakeCaseCommand): SExp => { 127 | return { 128 | type: 'list', 129 | data: [ 130 | { type: 'symbol', data: 'make-case' }, 131 | { type: 'integer', data: cmd.line }, 132 | { type: 'string', data: cmd.symbolName }, 133 | ], 134 | } 135 | } 136 | 137 | const metavariablesCommandToSExp = (cmd: MetavariablesComand): SExp => { 138 | return { 139 | type: 'list', 140 | data: [ 141 | { type: 'symbol', data: 'metavariables' }, 142 | { type: 'integer', data: cmd.width }, 143 | ], 144 | } 145 | } 146 | 147 | const proofSearchCommandToSExp = (cmd: ProofSearchCommand): SExp => { 148 | return { 149 | type: 'list', 150 | data: [ 151 | { type: 'symbol', data: 'proof-search' }, 152 | { type: 'integer', data: cmd.line }, 153 | { type: 'string', data: cmd.symbolName }, 154 | ], 155 | } 156 | } 157 | 158 | const printDefinitionCommandToSExp = (cmd: PrintDefinitionCommand): SExp => { 159 | return { 160 | type: 'list', 161 | data: [ 162 | { type: 'symbol', data: 'print-definition' }, 163 | { type: 'string', data: cmd.symbolName }, 164 | ], 165 | } 166 | } 167 | 168 | const browseNamespaceCommandToSepx = (cmd: BrowseNamespaceCommand): SExp => { 169 | return { 170 | type: 'list', 171 | data: [ 172 | { type: 'symbol', data: 'browse-namespace' }, 173 | { type: 'string', data: cmd.namespace }, 174 | ], 175 | } 176 | } 177 | 178 | const aproposCommandToSExp = (cmd: AproposCommand): SExp => { 179 | return { 180 | type: 'list', 181 | data: [ 182 | { type: 'symbol', data: 'apropos' }, 183 | { type: 'string', data: cmd.code }, 184 | ], 185 | } 186 | } 187 | 188 | const replCompletionCommandToSExp = (cmd: ReplCompletionsCommand): SExp => { 189 | return { 190 | type: 'list', 191 | data: [ 192 | { type: 'symbol', data: 'repl-completions' }, 193 | { type: 'string', data: cmd.name }, 194 | ], 195 | } 196 | } 197 | 198 | export const ideCommandToSExp = (cmd: IDECommand): SExp => { 199 | switch (cmd.type) { 200 | case 'interpret': { 201 | return interpretCommandToSExp(cmd) 202 | } 203 | case 'load-file': { 204 | return loadFileCommandToSExp(cmd) 205 | } 206 | case 'type-of': { 207 | return typeOfCommandToSExp(cmd) 208 | } 209 | case 'docs-for': { 210 | return docsForComandToSExp(cmd) 211 | } 212 | case 'case-split': { 213 | return caseSplitCommandToSExp(cmd) 214 | } 215 | case 'add-clause': { 216 | return addClauseCommandToSExp(cmd) 217 | } 218 | case 'add-proof-clause': { 219 | return addProofClauseCommandToSExp(cmd) 220 | } 221 | case 'make-with': { 222 | return makeWithCommandToSExp(cmd) 223 | } 224 | case 'make-lemma': { 225 | return makeLemmaCommandToSExp(cmd) 226 | } 227 | case 'make-case': { 228 | return makeCaseCommandToSExp(cmd) 229 | } 230 | case 'metavariables': { 231 | return metavariablesCommandToSExp(cmd) 232 | } 233 | case 'proof-search': { 234 | return proofSearchCommandToSExp(cmd) 235 | } 236 | case 'print-definition': { 237 | return printDefinitionCommandToSExp(cmd) 238 | } 239 | case 'browse-namespace': { 240 | return browseNamespaceCommandToSepx(cmd) 241 | } 242 | case 'apropos': { 243 | return aproposCommandToSExp(cmd) 244 | } 245 | case 'repl-completions': { 246 | return replCompletionCommandToSExp(cmd) 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /lib/idris-model.ts: -------------------------------------------------------------------------------- 1 | import { IdrisIdeMode } from './idris-ide-mode' 2 | import * as Rx from 'rx-lite' 3 | import * as JS from './utils/js' 4 | import * as path from 'path' 5 | import { CompilerOptions } from './utils/ipkg' 6 | import { IDECommand, SExp } from './protocol/ide-protocol' 7 | import { ideCommandToSExp } from './protocol/to-sexp' 8 | import Logger from './utils/Logger' 9 | 10 | export class IdrisModel { 11 | requestId = 0 12 | ideModeRef: IdrisIdeMode | null = null 13 | subjects: { [id: number]: Rx.Subject } = {} 14 | warnings: any = {} 15 | compilerOptions: CompilerOptions = { pkgs: [] } 16 | oldCompilerOptions: CompilerOptions = { pkgs: [] } 17 | 18 | constructor() { 19 | this.handleCommand = this.handleCommand.bind(this) 20 | } 21 | 22 | ideMode(compilerOptions: any) { 23 | if ( 24 | this.ideModeRef && 25 | (!JS.objectEqual(this.oldCompilerOptions, compilerOptions) || 26 | !this.ideModeRef.running()) 27 | ) { 28 | this.ideModeRef.stop() 29 | this.ideModeRef = null 30 | } 31 | if (!this.ideModeRef) { 32 | this.ideModeRef = new IdrisIdeMode() 33 | this.ideModeRef.on('message', this.handleCommand) 34 | this.ideModeRef.start(compilerOptions) 35 | this.oldCompilerOptions = compilerOptions 36 | } 37 | return this.ideModeRef 38 | } 39 | 40 | stop() { 41 | return this.ideModeRef != null ? this.ideModeRef.stop() : undefined 42 | } 43 | 44 | setCompilerOptions(options: CompilerOptions): void { 45 | this.compilerOptions = options 46 | } 47 | 48 | handleCommand(cmd: any) { 49 | if (cmd.length > 0) { 50 | const op = cmd[0], 51 | adjustedLength = Math.max(cmd.length, 2), 52 | params = cmd.slice(1, adjustedLength - 1), 53 | id = cmd[adjustedLength - 1] 54 | if (this.subjects[id] != null) { 55 | const subject = this.subjects[id] 56 | switch (op) { 57 | case ':return': 58 | var ret = params[0] 59 | if (ret[0] === ':ok') { 60 | const okparams = ret[1] 61 | if (okparams[0] === ':metavariable-lemma') { 62 | subject.onNext({ 63 | responseType: 'return', 64 | msg: okparams, 65 | }) 66 | } else { 67 | subject.onNext({ 68 | responseType: 'return', 69 | msg: ret.slice(1), 70 | }) 71 | } 72 | } else { 73 | subject.onError({ 74 | message: ret[1], 75 | warnings: this.warnings[id], 76 | highlightInformation: ret[2], 77 | cwd: this.compilerOptions.src, 78 | }) 79 | } 80 | subject.onCompleted() 81 | return delete this.subjects[id] 82 | case ':write-string': 83 | var msg = params[0] 84 | atom.notifications.addInfo(msg) 85 | return subject.onNext({ 86 | responseType: 'write-string', 87 | msg, 88 | }) 89 | case ':warning': 90 | var warning = params[0] 91 | return this.warnings[id].push(warning) 92 | case ':run-program': 93 | var options = { 94 | detail: 95 | 'The path for the compiled program. It was copied to your clipboard. Paste it into a terminal to execute.', 96 | dismissible: true, 97 | icon: 'comment', 98 | buttons: [{ text: 'Confirm' }], 99 | } 100 | atom.clipboard.write(params[0]) 101 | return atom.notifications.addSuccess(params[0], options) 102 | case ':set-prompt': 103 | // Ignore 104 | default: { 105 | Logger.logObject('Unhandled Operator', op) 106 | Logger.logObject('Params', params) 107 | return 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | getUID(): number { 115 | return ++this.requestId 116 | } 117 | 118 | prepareCommand(cmd: IDECommand): Rx.Subject { 119 | const id = this.getUID() 120 | const subject = new Rx.Subject() 121 | this.subjects[id] = subject 122 | this.warnings[id] = [] 123 | const command: SExp = { 124 | type: 'list', 125 | data: [ideCommandToSExp(cmd), { type: 'integer', data: id }], 126 | } 127 | this.ideMode(this.compilerOptions).send(command) 128 | return subject 129 | } 130 | 131 | changeDirectory(dir: string) { 132 | return this.interpret(`:cd ${dir}`) 133 | } 134 | 135 | load(uri: string) { 136 | const dir = this.compilerOptions.src 137 | ? this.compilerOptions.src 138 | : path.dirname(uri) 139 | 140 | const cd = (() => { 141 | if (dir !== this.compilerOptions.src) { 142 | this.compilerOptions.src = dir 143 | return this.changeDirectory(dir).map(() => dir) 144 | } else { 145 | return Rx.Observable.of(dir) 146 | } 147 | })() 148 | 149 | return cd.flatMap((_) => { 150 | return this.prepareCommand({ type: 'load-file', fileName: uri }) 151 | }) 152 | } 153 | 154 | docsFor(symbolName: string) { 155 | return this.prepareCommand({ 156 | type: 'docs-for', 157 | symbolName, 158 | mode: 'full', 159 | }) 160 | } 161 | 162 | replCompletions(name: string) { 163 | return this.prepareCommand({ type: 'repl-completions', name }) 164 | } 165 | 166 | getType(code: string) { 167 | return this.prepareCommand({ type: 'type-of', code }) 168 | } 169 | 170 | caseSplit(line: number, symbolName: string) { 171 | return this.prepareCommand({ type: 'case-split', line, symbolName }) 172 | } 173 | 174 | makeWith(line: number, symbolName: string) { 175 | return this.prepareCommand({ type: 'make-with', line, symbolName }) 176 | } 177 | 178 | makeLemma(line: number, symbolName: string) { 179 | return this.prepareCommand({ type: 'make-lemma', line, symbolName }) 180 | } 181 | 182 | interpret(code: string) { 183 | return this.prepareCommand({ type: 'interpret', code }) 184 | } 185 | 186 | makeCase(line: number, symbolName: string) { 187 | return this.prepareCommand({ type: 'make-case', line, symbolName }) 188 | } 189 | 190 | addClause(line: number, symbolName: string) { 191 | return this.prepareCommand({ type: 'add-clause', line, symbolName }) 192 | } 193 | 194 | addProofClause(line: number, symbolName: string) { 195 | return this.prepareCommand({ 196 | type: 'add-proof-clause', 197 | line, 198 | symbolName, 199 | }) 200 | } 201 | 202 | holes(width: number) { 203 | return this.prepareCommand({ type: 'metavariables', width }) 204 | } 205 | 206 | proofSearch(line: number, symbolName: string) { 207 | return this.prepareCommand({ type: 'proof-search', line, symbolName }) 208 | } 209 | 210 | printDefinition(symbolName: string) { 211 | return this.prepareCommand({ type: 'print-definition', symbolName }) 212 | } 213 | 214 | apropos(code: string) { 215 | return this.prepareCommand({ type: 'apropos', code }) 216 | } 217 | 218 | browseNamespace(namespace: string) { 219 | return this.prepareCommand({ type: 'browse-namespace', namespace }) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Language-Idris Changelog 2 | 3 | ## Next version 4 | 5 | ### Added 6 | 7 | ### Fixed 8 | 9 | ## v0.4.10 10 | 11 | ### Added 12 | - Display typechecking information 13 | - Copy executable path to clipboard 14 | - Syntax highlitning in message panel 15 | - Proof clause 16 | - higlightning of parameters 17 | - Better support for literate idris 18 | - Added snippets 19 | 20 | ### Fixed 21 | - Display error message from idris 22 | - Idris now restarts if stopped 23 | - fix comments highlightning 24 | 25 | ## v0.4.9 26 | 27 | ### Added 28 | 29 | - Allow incomplete data definition 30 | - Add record and rewrite keywords 31 | - Support literate files 32 | - Add cast snippet 33 | 34 | ### Fixed 35 | 36 | - Indent after do/where/if/then/else/of 37 | - Save file state when executing commands 38 | 39 | ## v0.4.8 40 | 41 | ### Fixed 42 | - Fix proof search 43 | - Remove deprecated selector 44 | 45 | ## v0.4.7 46 | 47 | ### Added 48 | 49 | - REPL and package documentation [#146](https://github.com/idris-hackers/atom-language-idris/pull/146) (@allancto) 50 | - Additional commands in menu [#142](https://github.com/idris-hackers/atom-language-idris/pull/142) (@joheinz) 51 | 52 | ### Fixed 53 | 54 | - Crashes with Atom 1.13.0 [#149](https://github.com/idris-hackers/atom-language-idris/pull/149) (@melted) 55 | - 'import public' syntax [#145](https://github.com/idris-hackers/atom-language-idris/pull/145) (@joheinz) 56 | 57 | ## v0.4.6 58 | 59 | ### Added 60 | 61 | - Report type checking status 62 | - Snippets 63 | - Navigate to error 64 | - Highlight error 65 | 66 | ### Fixed 67 | 68 | - Styling of error messages 69 | - ipkg grammar 70 | 71 | ## v0.4.5 72 | 73 | ### Added 74 | 75 | ### Fixed 76 | 77 | - Many different processes are spawned [#114](https://github.com/idris-hackers/atom-language-idris/issues/114) (@pyrtsa) 78 | - use 'cmd' instead of 'alt' on osx (@karljs) 79 | 80 | ## v0.4.4 81 | 82 | ### Added 83 | 84 | - Make apropos view results scrollable [#109](https://github.com/idris-hackers/atom-language-idris/pull/109) (@justjoheinz) 85 | - Codecompletion via autocomplete-plus [#98](https://github.com/idris-hackers/atom-language-idris/pull/98) (@justjoheinz) 86 | - Replace deprecated abstract keyword with export [#101](https://github.com/idris-hackers/atom-language-idris/pull/101) (@justjoheinz) 87 | - Add snippets for the most common interface implementations [#99](https://github.com/idris-hackers/atom-language-idris/pull/99) (@justjoheinz) 88 | 89 | ### Fixed 90 | 91 | - Nested with blocks [#112](https://github.com/idris-hackers/atom-language-idris/issues/112) 92 | - Fix passing packages in ipkg files [#111](https://github.com/idris-hackers/atom-language-idris/pull/111) 93 | 94 | ## v0.4.3 95 | 96 | ### Added 97 | 98 | ### Fixed 99 | 100 | - After closing panel once with "X" icon, it no longer appears for type checking/inspection [#90](https://github.com/idris-hackers/atom-language-idris/issues/90) (@justjoheinz) 101 | 102 | ## v0.4.2 103 | 104 | ### Added 105 | 106 | - syntax highlighting for constructors [#95](https://github.com/idris-hackers/atom-language-idris/pull/95) 107 | 108 | ### Fixed 109 | 110 | ## v0.4.1 111 | 112 | ### Added 113 | 114 | - class and instance to interface and implementation (@justjoheinz) 115 | 116 | ### Fixed 117 | 118 | ## v0.4.0 119 | 120 | ### Added 121 | 122 | - Added a REPL [#80](https://github.com/idris-hackers/atom-language-idris/pull/80) 123 | - Added a panel for the `apropos` command 124 | 125 | ### Fixed 126 | 127 | ## v0.3.4 128 | 129 | ### Added 130 | 131 | ### Fixed 132 | 133 | - Added a better description for the path to the idris executable in the settings 134 | - Fixed the highlighting of comments 135 | - Fix the highlighting of documentation comments [#82](https://github.com/idris-hackers/atom-language-idris/pull/82) (@geo2a) 136 | - Initial clause is inserted in wrong place for functions with multiline type annotation. [#72](https://github.com/idris-hackers/atom-language-idris/issues/72) 137 | 138 | ## v0.3.3 139 | 140 | ### Added 141 | 142 | ### Fixed 143 | 144 | - Most commands throw exceptions on Windows 8.1 (@aochagavia) 145 | 146 | ## v0.3.2 147 | 148 | ### Added 149 | 150 | - parse the new `pkgs`option in the ipkg file [idris-lang/Idris-dev/pull/2668](https://github.com/idris-lang/Idris-dev/pull/2668) 151 | 152 | ### Fixed 153 | 154 | ## v0.3.1 155 | 156 | ### Added 157 | 158 | ### Fixed 159 | 160 | - get documentation or types for operators [#66](https://github.com/idris-hackers/atom-language-idris/issues/66) 161 | - removed the statusbar [#67](https://github.com/idris-hackers/atom-language-idris/issues/67) 162 | 163 | ## v0.3.0 164 | 165 | ### Added 166 | 167 | - Add a means of setting the Idris -p option [#29](https://github.com/idris-hackers/atom-language-idris/issues/29) 168 | 169 | ### Fixed 170 | 171 | ## v0.2.5 172 | 173 | ### Added 174 | 175 | - Restart the idris compiler after every commmand if it was killed [#54](https://github.com/idris-hackers/atom-language-idris/pull/54) 176 | - added the ability to style all the idris-panels 177 | 178 | ### Fixed 179 | 180 | - Status message should appear only in idris projects [#52](https://github.com/idris-hackers/atom-language-idris/issues/52) many thanks to @jeremy-w 181 | 182 | ## v0.2.4 183 | 184 | ### Added 185 | 186 | ### Fixed 187 | 188 | - Uncaught ReferenceError: editor is not defined [#49](https://github.com/idris-hackers/atom-language-idris/issues/49) 189 | - Error when searching for type, documentation [#37](https://github.com/idris-hackers/atom-language-idris/issues/37) 190 | 191 | ## v0.2.3 192 | 193 | ### Added 194 | 195 | - make-with (@edwinb) 196 | - make-case (@edwinb) 197 | - make-lemma (@edwinb) 198 | 199 | ### Fixed 200 | 201 | - Uncaught Error: Can't save buffer with no file path [#47](https://github.com/idris-hackers/atom-language-idris/issues/47) 202 | - save files before executing a command (@edwinb) 203 | - The Idris Errors panel should tell me if typechecking went successfully [#43](https://github.com/idris-hackers/atom-language-idris/issues/43) (@edwinb) 204 | 205 | ## v0.2.2 206 | 207 | ### Added 208 | 209 | ### Fixed 210 | 211 | - fix the new error messages 212 | 213 | ## v0.2.1 214 | 215 | ### Added 216 | 217 | - `print-definition` to show the definition of the selected word 218 | - add error messages when the compiler crashes/can't be found 219 | 220 | ### Fixed 221 | 222 | ## v0.2.0 223 | 224 | ### Added 225 | 226 | - status indicator that shows if a file is loaded or dirty 227 | - metavariables are now called holes 228 | 229 | ### Fixed 230 | 231 | - fixed bug in the parser when there where backslashes in the answer [#32](https://github.com/idris-hackers/atom-language-idris/issues/32) (@david-christiansen) 232 | - Program not loaded before running interactive editing commands [#34](https://github.com/idris-hackers/atom-language-idris/issues/34) 233 | - faster startup [#28](https://github.com/idris-hackers/atom-language-idris/issues/28) 234 | 235 | ## v0.1.4 236 | 237 | ### Added 238 | 239 | - new metavariable view (`Language Idris: Metavariables`) 240 | - a tutorial that explains how to use this package 241 | - context menu for `Language Idris: Type Of` and `Language Idris: Docs For` 242 | 243 | ### Fixed 244 | 245 | - `Language Idris: Proof Search` and `Language Idris: Add Clause` 246 | - deprecations that now broke the editor because of the dropped API methods 247 | 248 | ## v0.1.3 249 | 250 | ### Added 251 | 252 | ### Fixed 253 | 254 | - Better syntax highlighting 255 | - fixed the parser for the ide-mode lisp 256 | - fixed [#18](https://github.com/idris-hackers/atom-language-idris/issues/18) 257 | - fixed [#19](https://github.com/idris-hackers/atom-language-idris/issues/19) 258 | - fixed an issue with the error lines not being clickable in the error panel 259 | 260 | ## v0.1.1 261 | 262 | ### Added 263 | 264 | - Type and doc info highlighting https://github.com/idris-hackers/atom-language-idris/pull/9 (@archaeron) 265 | 266 | ### Fixed 267 | 268 | - Ensure that keybindings only work on Idris files (#2) 269 | - Syntax highlighting for infix functions 270 | - Fixed a crash when idris was not installed 271 | 272 | ## v0.1.0 273 | 274 | ### Added 275 | 276 | - Shortcut to Show the doc of a variable (ctrl-alt-d) 277 | 278 | ### Fixed 279 | 280 | - updated for the new version of Atom (@archaeron) 281 | - new parser for the ide-mode commands (@archaeron) 282 | - new serializer for the ide-mode commands (@archaeron) 283 | - various fixes (@ulidtko) 284 | 285 | ## v0.0.1 286 | 287 | ### Added 288 | 289 | - Case-splitting (ctrl-alt-c) (@fangel) 290 | - Clause-adding (ctrl-alt-a) (@fangel) 291 | - Proof-search (ctrl-alt-s) (@fangel) 292 | - Showing the types of meta-variables (ctrl-alt-t) (@fangel) 293 | - Show the doc of a variable (@fangel) 294 | -------------------------------------------------------------------------------- /grammars/language-idris.cson: -------------------------------------------------------------------------------- 1 | name: 'Idris' 2 | scopeName: 'source.idris' 3 | fileTypes: ['idr'] 4 | patterns: 5 | [ 6 | { 7 | name: 'keyword.control.idris' 8 | match: '\\b(if|then|else|do|let|in|codata|record|dsl)\\b' 9 | } 10 | { 11 | name: 'keyword.control.idris' 12 | match: '\\b(impossible|case|of|total|partial|mutual|infix|infixl|infixr)\\b' 13 | } 14 | { 15 | name: 'keyword.control.idris' 16 | match: '\\b(where|with|syntax|proof|postulate|using|namespace|class|instance|interface|implementation|record|rewrite)\\b' 17 | } 18 | { 19 | name: 'keyword.control.idris' 20 | match: '\\b(public|private|export|implicit)\\b' 21 | } 22 | { 23 | name: 'comment.line.idris' 24 | match: '(--).*$\n?' 25 | comment: 'Line comment' 26 | } 27 | { 28 | name: 'comment.documentation.line.idris' 29 | match: '(\\|\\|\\|).*$\n?' 30 | comment: 'Line comment' 31 | } 32 | { 33 | name: 'storage.type.function.idris' 34 | match: '\\?[-!#\\$%&\\*\\+\\.\\/<=>@\\\\\^\|~:]+|[-!#\\$%&\\*\\+\\.\\/<=>@\\\\\^\|~:\\?][-!#\\$%&\\*\\+\\.\\/<=>@\\\\\^\|~:]*' 35 | } 36 | { 37 | name: 'storage.type.builtin.idris' 38 | match: '\\b(Type|Int|Nat|Integer|Float|Char|String|Ptr|Bits8|Bits16|Bits32|Bits64|Bool)\\b' 39 | } 40 | { 41 | name: 'constant.numeric.idris' 42 | match: '\\b(S|Z)\\b' 43 | } 44 | { 45 | match: '\\b([0-9]+|0([xX][0-9a-fA-F]+|[oO][0-7]+))\\b' 46 | name: 'constant.numeric.idris' 47 | comment: 'integer literal' 48 | } 49 | { 50 | match: '\\b([0-9]+\\.[0-9]+([eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+)\\b' 51 | name: 'constant.numeric.float.idris' 52 | comment: 'float literal' 53 | } 54 | { 55 | match: '\\(\\)' 56 | name: 'constant.unit.idris' 57 | } 58 | { 59 | name: 'comment.block.idris' 60 | begin: '\\{-' 61 | end: '-\\}' 62 | comment: 'Block comment' 63 | } 64 | { 65 | name: 'string.quoted.double.idris' 66 | begin: '"' 67 | beginCaptures: 68 | 0: 69 | name: 'punctuation.definition.string.begin.idris' 70 | end: '"' 71 | endCaptures: 72 | 0: 73 | name: 'punctuation.definition.string.end.idris' 74 | patterns: 75 | [ 76 | { 77 | include: '#escape_characters' 78 | } 79 | ] 80 | } 81 | { 82 | name: 'variable.other.idris' 83 | match : "(?\\\\*]+" 204 | } 205 | { 206 | include: '#data_ctor' 207 | } 208 | ] 209 | repository: 210 | context_signature: 211 | patterns: 212 | [ 213 | { 214 | name: 'meta.context-signature.idris' 215 | match: "([\\w._']+)((\\s+[\\w_']+)+)\\s*(=>)" 216 | captures: 217 | 1: 218 | name: 'entity.other.inherited-class.idris' 219 | 2: 220 | name: 'entity.other.attribute-name.idris' 221 | 4: 222 | name: 'keyword.operator.double-arrow.idris' 223 | } 224 | { 225 | name: 'meta.context-signature.idris' 226 | comment: 227 | """For things like '(Eq a, Show b) =>' 228 | It begins with '(' either followed by ') =>' on the same line, 229 | or anything but ')' until the end of line.""" 230 | begin: "(\\()((?=.*\\)\\s*=>)|(?=[^)]*$))" 231 | beginCaptures: 232 | 1: 233 | name: 'punctuation.context.begin.idris' 234 | end: "(\\))\\s*(=>)" 235 | endCaptures: 236 | 1: 237 | name: 'punctuation.context.end.idris' 238 | 2: 239 | name: 'keyword.operator.double-arrow.idris' 240 | patterns: 241 | [ 242 | { 243 | name: 'meta.class-constraint.idris' 244 | match: "([\\w']+)\\s+([\\w']+)" 245 | captures: 246 | 1: 247 | name: 'entity.other.inherited-class.idris' 248 | 2: 249 | name: 'entity.other.attribute-name.idris' 250 | } 251 | ] 252 | } 253 | ] 254 | parameter_type: 255 | comment: "Parameter types in a type signature" 256 | patterns: 257 | [ 258 | { 259 | include: '#prelude_type' 260 | } 261 | { 262 | name: 'meta.parameter.named.idris' 263 | comment: '(x : Nat)' 264 | begin: "\\(([\\w']+)\\s*:(?!:)" 265 | beginCaptures: 266 | 1: 267 | name: 'entity.name.tag.idris' 268 | end: "\\)" 269 | patterns: 270 | [ 271 | { 272 | include: '#prelude_type' 273 | } 274 | ] 275 | } 276 | { 277 | name: 'meta.parameter.implicit.idris' 278 | comment: '{auto p : a = b}' 279 | begin: "\\{((auto|default .+)\\s+)?([\\w']+)\\s*:(?!:)" 280 | beginCaptures: 281 | 1: 282 | name: 'storage.modifier.idris' 283 | 3: 284 | name: 'entity.name.tag.idris' 285 | end: '\\}' 286 | patterns: 287 | [ 288 | { 289 | include: '#prelude_type' 290 | } 291 | ] 292 | } 293 | ] 294 | type_signature: 295 | patterns: 296 | [ 297 | { 298 | name: 'keyword.operator.arrow.idris' 299 | match: '->' 300 | } 301 | { 302 | include: '#context_signature' 303 | } 304 | { 305 | include: '#parameter_type' 306 | } 307 | { 308 | include: '#language_const' 309 | } 310 | ] 311 | function_signature: 312 | name: 'meta.function.type-signature.idris' 313 | begin: "(([\\w']+)|\\(([|!%$+\\-.,=:]+)\\))\\s*(:)(?!:)" 314 | beginCaptures: 315 | 2: 316 | name: 'entity.name.function.idris' 317 | 3: 318 | name: 'entity.name.function.idris' 319 | 4: 320 | name: 'keyword.operator.colon.idris' 321 | end: "(;|(?=--)|(?<=[^\\s>])\\s*(?!->)\\s*$)" 322 | patterns: 323 | [ 324 | { 325 | include: '#type_signature' 326 | } 327 | ] 328 | comment: """The end patterm is a bit tricky. It's either ';' or something, at the end of the line, 329 | but not '->', because a type signature can be multiline. Though, it doesn't help, if you 330 | break the signature before arrows.""" 331 | language_const: 332 | patterns: 333 | [ 334 | { 335 | name: 'constant.language.unit.idris' 336 | match: "\\(\\)" 337 | } 338 | { 339 | name: 'constant.language.bottom.idris' 340 | match: "_\\|_" 341 | } 342 | { 343 | name: 'constant.language.underscore.idris' 344 | match: "\\b_\\b" 345 | } 346 | ] 347 | escape_characters: 348 | patterns: 349 | [ 350 | { 351 | name: 'constant.character.escape.ascii.idris' 352 | match: '\\\\(NUL|SOH|STX|ETX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|SO|SI|DLE|DC1|DC2|DC3|DC4|NAK|SYN|ETB|CAN|EM|SUB|ESC|FS|GS|RS|US|SP|DEL|[abfnrtv\\\\\\"\'\\&])' 353 | } 354 | { 355 | name: 'constant.character.escape.octal.idris' 356 | match: '\\\\o[0-7]+|\\\\x[0-9A-Fa-f]+|\\\\[0-9]+' 357 | } 358 | { 359 | name: 'constant.character.escape.control.idris' 360 | match: '\\^[A-Z@\\[\\]\\\\\\^_]' 361 | } 362 | ] 363 | data_ctor: 364 | patterns: 365 | [ 366 | { 367 | 'name': 'entity.name.tag.idris' 368 | 'match': '\\b[\\p{Lu}\\p{Lt}][\\p{Ll}_\\p{Lu}\\p{Lt}\\p{Nd}\']*(?:\\.[\\p{Lu}\\p{Lt}][\\p{Ll}_\\p{Lu}\\p{Lt}\\p{Nd}\']*)*\\b' 369 | } 370 | ] 371 | uuid: '4dd16092-ffa5-4ba4-8075-e5da9f368a72' 372 | -------------------------------------------------------------------------------- /lib/idris-controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MessagePanelView, 3 | PlainMessageView, 4 | LineMessageView, 5 | } from 'atom-message-panel' 6 | import { InformationView } from './views/information-view' 7 | import { HolesView } from './views/holes-view' 8 | import Logger from './utils/Logger' 9 | import { IdrisModel } from './idris-model' 10 | import * as Ipkg from './utils/ipkg' 11 | import * as Symbol from './utils/symbol' 12 | import { getWordUnderCursor, moveToNextEmptyLine } from './utils/editor' 13 | import * as highlighter from './utils/highlighter' 14 | import { 15 | TextEditor, 16 | RangeCompatible, 17 | DisplayMarker, 18 | Pane, 19 | WorkspaceOpenOptions, 20 | } from 'atom' 21 | 22 | export class IdrisController { 23 | errorMarkers: Array = [] 24 | model: IdrisModel = new IdrisModel() 25 | messages: MessagePanelView = new MessagePanelView({ 26 | title: 'Idris Messages', 27 | }) 28 | 29 | constructor() { 30 | this.prefixLiterateClause = this.prefixLiterateClause.bind(this) 31 | this.clearMessagePanel = this.clearMessagePanel.bind(this) 32 | this.hideAndClearMessagePanel = this.hideAndClearMessagePanel.bind(this) 33 | this.stopCompiler = this.stopCompiler.bind(this) 34 | this.runCommand = this.runCommand.bind(this) 35 | this.provideReplCompletions = this.provideReplCompletions.bind(this) 36 | this.typecheckFile = this.typecheckFile.bind(this) 37 | this.getDocsForWord = this.getDocsForWord.bind(this) 38 | this.getTypeForWord = this.getTypeForWord.bind(this) 39 | this.doCaseSplit = this.doCaseSplit.bind(this) 40 | this.doAddClause = this.doAddClause.bind(this) 41 | this.doAddProofClause = this.doAddProofClause.bind(this) 42 | this.doMakeWith = this.doMakeWith.bind(this) 43 | this.doMakeLemma = this.doMakeLemma.bind(this) 44 | this.doMakeCase = this.doMakeCase.bind(this) 45 | this.showHoles = this.showHoles.bind(this) 46 | this.doProofSearch = this.doProofSearch.bind(this) 47 | this.doBrowseNamespace = this.doBrowseNamespace.bind(this) 48 | this.printDefinition = this.printDefinition.bind(this) 49 | this.openREPL = this.openREPL.bind(this) 50 | this.apropos = this.apropos.bind(this) 51 | this.displayErrors = this.displayErrors.bind(this) 52 | } 53 | 54 | getCommands() { 55 | return { 56 | 'language-idris:type-of': this.runCommand(this.getTypeForWord), 57 | 'language-idris:docs-for': this.runCommand(this.getDocsForWord), 58 | 'language-idris:case-split': this.runCommand(this.doCaseSplit), 59 | 'language-idris:add-clause': this.runCommand(this.doAddClause), 60 | 'language-idris:make-with': this.runCommand(this.doMakeWith), 61 | 'language-idris:make-lemma': this.runCommand(this.doMakeLemma), 62 | 'language-idris:make-case': this.runCommand(this.doMakeCase), 63 | 'language-idris:holes': this.runCommand(this.showHoles), 64 | 'language-idris:proof-search': this.runCommand(this.doProofSearch), 65 | 'language-idris:typecheck': this.runCommand(this.typecheckFile), 66 | 'language-idris:print-definition': this.runCommand( 67 | this.printDefinition, 68 | ), 69 | // 'language-idris:stop-compiler': this.stopCompiler, 70 | 'language-idris:open-repl': this.runCommand(this.openREPL), 71 | 'language-idris:apropos': this.runCommand(this.apropos), 72 | 'language-idris:add-proof-clause': this.runCommand( 73 | this.doAddProofClause, 74 | ), 75 | 'language-idris:browse-namespace': this.runCommand( 76 | this.doBrowseNamespace, 77 | ), 78 | 'language-idris:close-information-view': this 79 | .hideAndClearMessagePanel, 80 | } 81 | } 82 | 83 | // check if this is a literate idris file 84 | isLiterateGrammar(): boolean { 85 | return ( 86 | this.getEditor()?.getGrammar().scopeName === 'source.idris.literate' 87 | ) 88 | } 89 | 90 | // prefix code lines with "> " if we are in the literate grammar 91 | prefixLiterateClause(clause: Array): Array { 92 | const birdPattern = new RegExp(`^\ 93 | >\ 94 | (\\s)+\ 95 | `) 96 | 97 | if (this.isLiterateGrammar()) { 98 | return Array.from(clause).map((line: string) => 99 | line.match(birdPattern) ? line : '> ' + line, 100 | ) 101 | } else { 102 | return clause 103 | } 104 | } 105 | 106 | createMarker( 107 | editor: TextEditor, 108 | range: RangeCompatible, 109 | type: 110 | | 'line' 111 | | 'line-number' 112 | | 'text' 113 | | 'highlight' 114 | | 'overlay' 115 | | 'gutter' 116 | | 'block' 117 | | 'cursor', 118 | ): DisplayMarker { 119 | const marker = editor.markBufferRange(range, { invalidate: 'never' }) 120 | editor.decorateMarker(marker, { 121 | type, 122 | class: 'highlight-idris-error', 123 | }) 124 | return marker 125 | } 126 | 127 | destroyMarkers(): void { 128 | Array.from(this.errorMarkers).map((marker) => marker.destroy()) 129 | } 130 | 131 | destroy(): void { 132 | if (this.model) { 133 | Logger.logText('Idris: Shutting down!') 134 | this.model.stop() 135 | } 136 | } 137 | 138 | // clear the message panel and optionally display a new title 139 | clearMessagePanel(title?: string): void { 140 | if (this.messages) { 141 | this.messages.attach() 142 | this.messages.show() 143 | this.messages.clear() 144 | if (title) { 145 | this.messages.setTitle(title, true) 146 | } 147 | } 148 | } 149 | 150 | // hide the message panel 151 | hideAndClearMessagePanel(): void { 152 | if (this.messages) { 153 | this.clearMessagePanel() 154 | this.messages.hide() 155 | } 156 | } 157 | 158 | // add raw information to the message panel 159 | rawMessage(text: string): void { 160 | this.messages.add( 161 | new PlainMessageView({ 162 | raw: true, 163 | message: '
' + text + '
', 164 | className: 'preview', 165 | }), 166 | ) 167 | } 168 | 169 | initialize(compilerOptions: Ipkg.CompilerOptions): void { 170 | this.destroyMarkers() 171 | this.messages.attach() 172 | this.messages.hide() 173 | this.model.setCompilerOptions(compilerOptions) 174 | } 175 | 176 | /** 177 | * Get the currently active text editor. 178 | */ 179 | getEditor(): TextEditor | undefined { 180 | return atom.workspace.getActiveTextEditor() 181 | } 182 | 183 | insertNewlineWithoutAutoIndent(editor: TextEditor): void { 184 | editor.insertText('\n', { autoIndentNewline: false }) 185 | } 186 | 187 | getPane(): Pane { 188 | return atom.workspace.getActivePane() 189 | } 190 | 191 | stopCompiler(): boolean | undefined { 192 | return this.model != null ? this.model.stop() : undefined 193 | } 194 | 195 | runCommand(command: (args: any) => void) { 196 | return (args: any) => { 197 | const compilerOptions = Ipkg.compilerOptions(atom.project) 198 | return compilerOptions.subscribe((options: any) => { 199 | Logger.logObject('Compiler Options:', options) 200 | this.initialize(options) 201 | return command(args) 202 | }) 203 | } 204 | } 205 | 206 | // see https://github.com/atom/autocomplete-plus/wiki/Provider-API 207 | provideReplCompletions() { 208 | return { 209 | selector: '.source.idris', 210 | 211 | inclusionPriority: 1, 212 | excludeLowerPriority: false, 213 | 214 | // Get suggestions from the Idris REPL. You can always ask for suggestions 215 | // or type at least 3 characters to get suggestions based on your autocomplete-plus 216 | // settings. 217 | getSuggestions: ({ prefix, activatedManually }: any) => { 218 | const trimmedPrefix = prefix.trim() 219 | if (trimmedPrefix.length > 2 || activatedManually) { 220 | return Ipkg.compilerOptions(atom.project) 221 | .flatMap((options: any) => { 222 | this.initialize(options) 223 | return this.model.replCompletions(trimmedPrefix) 224 | }) 225 | .toPromise() 226 | .then(({ msg }: any) => 227 | Array.from(msg[0][0]).map((sug) => ({ 228 | type: 'function', 229 | text: sug, 230 | })), 231 | ) 232 | } else { 233 | return null 234 | } 235 | }, 236 | } 237 | } 238 | 239 | saveFile(editor: TextEditor | undefined): Promise { 240 | if (editor) { 241 | const path = editor.getPath() 242 | if (path) { 243 | return editor.save().then(() => path) 244 | } else { 245 | const pane = this.getPane() 246 | const savePromise = pane.saveActiveItemAs() 247 | if (savePromise) { 248 | const newPath = editor.getPath() 249 | if (newPath) { 250 | return savePromise.then(() => newPath) 251 | } else { 252 | return Promise.reject() 253 | } 254 | } else { 255 | return Promise.reject() 256 | } 257 | } 258 | } else { 259 | return Promise.reject() 260 | } 261 | } 262 | 263 | typecheckFile() { 264 | const editor = this.getEditor() 265 | if (editor) { 266 | return this.saveFile(editor).then((uri) => { 267 | this.clearMessagePanel('Idris: Typechecking...') 268 | 269 | const successHandler = () => { 270 | return this.clearMessagePanel( 271 | 'Idris: File loaded successfully', 272 | ) 273 | } 274 | 275 | return this.model 276 | .load(uri) 277 | .filter( 278 | ({ responseType }: any) => responseType === 'return', 279 | ) 280 | .subscribe(successHandler, this.displayErrors) 281 | }) 282 | } 283 | } 284 | 285 | getDocsForWord() { 286 | const editor = this.getEditor() 287 | if (editor) { 288 | return this.saveFile(editor).then((uri) => { 289 | const word = Symbol.serializeWord(getWordUnderCursor(editor)) 290 | this.clearMessagePanel( 291 | 'Idris: Searching docs for ' + word + ' ...', 292 | ) 293 | 294 | const successHandler = ({ msg }: any) => { 295 | const [type, highlightingInfo] = Array.from(msg) 296 | this.clearMessagePanel( 297 | 'Idris: Docs for ' + word + '', 298 | ) 299 | 300 | const informationView = new InformationView() 301 | informationView.initialize({ 302 | obligation: type, 303 | highlightingInfo, 304 | }) 305 | return this.messages.add(informationView) 306 | } 307 | 308 | return this.model 309 | .load(uri) 310 | .filter( 311 | ({ responseType }: any) => responseType === 'return', 312 | ) 313 | .flatMap(() => this.model.docsFor(word)) 314 | .catch(() => this.model.docsFor(word)) 315 | .subscribe(successHandler, this.displayErrors) 316 | }) 317 | } 318 | } 319 | 320 | getTypeForWord() { 321 | const editor = this.getEditor() 322 | if (editor) { 323 | return this.saveFile(editor).then((uri) => { 324 | const word = Symbol.serializeWord(getWordUnderCursor(editor)) 325 | this.clearMessagePanel( 326 | 'Idris: Searching type of ' + word + ' ...', 327 | ) 328 | const successHandler = ({ msg }: any): void => { 329 | const [type, highlightingInfo] = msg 330 | this.clearMessagePanel( 331 | 'Idris: Type of ' + word + '', 332 | ) 333 | const informationView = new InformationView() 334 | informationView.initialize({ 335 | obligation: type, 336 | highlightingInfo, 337 | }) 338 | this.messages.add(informationView) 339 | } 340 | return this.model 341 | .load(uri) 342 | .filter((response: any) => { 343 | return response.responseType === 'return' 344 | }) 345 | .flatMap(() => this.model.getType(word)) 346 | .subscribe(successHandler, this.displayErrors) 347 | }) 348 | } 349 | } 350 | 351 | doCaseSplit() { 352 | const editor = this.getEditor() 353 | if (editor) { 354 | return this.saveFile(editor).then((uri) => { 355 | const cursor = editor.getLastCursor() 356 | const line = cursor.getBufferRow() 357 | const word = getWordUnderCursor(editor) 358 | 359 | this.clearMessagePanel('Idris: Do case split ...') 360 | 361 | const successHandler = ({ msg }: any) => { 362 | const [split] = msg 363 | if (split === '') { 364 | // split returned nothing - cannot split 365 | return this.clearMessagePanel( 366 | 'Idris: Cannot split ' + word, 367 | ) 368 | } else { 369 | this.hideAndClearMessagePanel() 370 | const lineRange = cursor.getCurrentLineBufferRange({ 371 | includeNewline: true, 372 | }) 373 | return editor.setTextInBufferRange(lineRange, split) 374 | } 375 | } 376 | 377 | return this.model 378 | .load(uri) 379 | .filter( 380 | ({ responseType }: any) => responseType === 'return', 381 | ) 382 | .flatMap(() => this.model.caseSplit(line + 1, word)) 383 | .subscribe(successHandler, this.displayErrors) 384 | }) 385 | } 386 | } 387 | 388 | /** 389 | * Add a new clause to a function. 390 | */ 391 | doAddClause() { 392 | const editor = this.getEditor() 393 | if (editor) { 394 | return this.saveFile(editor).then((uri) => { 395 | const line = editor.getLastCursor().getBufferRow() 396 | // by adding a clause we make sure that the word is 397 | // not treated as a symbol 398 | const word = getWordUnderCursor(editor) 399 | 400 | this.clearMessagePanel('Idris: Add clause ...') 401 | 402 | const successHandler = ({ msg }: any) => { 403 | const [clause] = this.prefixLiterateClause(msg) 404 | 405 | this.hideAndClearMessagePanel() 406 | 407 | editor.transact(() => { 408 | moveToNextEmptyLine(editor) 409 | 410 | // Insert the new clause 411 | editor.insertText(clause) 412 | 413 | // And move the cursor to the beginning of 414 | // the new line and add an empty line below it 415 | editor.insertNewlineBelow() 416 | editor.moveUp() 417 | editor.moveToBeginningOfLine() 418 | }) 419 | } 420 | 421 | return this.model 422 | .load(uri) 423 | .filter( 424 | ({ responseType }: any) => responseType === 'return', 425 | ) 426 | .flatMap(() => this.model.addClause(line + 1, word)) 427 | .subscribe(successHandler, this.displayErrors) 428 | }) 429 | } 430 | } 431 | 432 | /** 433 | * Use special syntax for proof obligation clauses. 434 | */ 435 | doAddProofClause() { 436 | const editor = this.getEditor() 437 | if (editor) { 438 | return this.saveFile(editor).then((uri) => { 439 | const line = editor.getLastCursor().getBufferRow() 440 | const word = getWordUnderCursor(editor) 441 | this.clearMessagePanel('Idris: Add proof clause ...') 442 | 443 | const successHandler = ({ msg }: any) => { 444 | const [clause] = this.prefixLiterateClause(msg) 445 | 446 | this.hideAndClearMessagePanel() 447 | 448 | editor.transact(() => { 449 | moveToNextEmptyLine(editor) 450 | 451 | // Insert the new clause 452 | editor.insertText(clause) 453 | 454 | // And move the cursor to the beginning of 455 | // the new line and add an empty line below it 456 | editor.insertNewlineBelow() 457 | editor.moveUp() 458 | editor.moveToBeginningOfLine() 459 | }) 460 | } 461 | 462 | return this.model 463 | .load(uri) 464 | .filter( 465 | ({ responseType }: any) => responseType === 'return', 466 | ) 467 | .flatMap(() => this.model.addProofClause(line + 1, word)) 468 | .subscribe(successHandler, this.displayErrors) 469 | }) 470 | } 471 | } 472 | 473 | // add a with view 474 | doMakeWith() { 475 | const editor = this.getEditor() 476 | if (editor) { 477 | return this.saveFile(editor).then((uri) => { 478 | const line = editor.getLastCursor().getBufferRow() 479 | const word = getWordUnderCursor(editor) 480 | 481 | this.clearMessagePanel('Idris: Make with view ...') 482 | 483 | const successHandler = ({ msg }: any) => { 484 | const [clause] = Array.from(this.prefixLiterateClause(msg)) 485 | 486 | this.hideAndClearMessagePanel() 487 | 488 | return editor.transact(() => { 489 | // Delete old line, insert the new with block 490 | editor.deleteLine() 491 | editor.moveToBeginningOfLine() 492 | editor.insertText(clause) 493 | this.insertNewlineWithoutAutoIndent(editor) 494 | // And move the cursor to the beginning of 495 | // the new line 496 | editor.moveToBeginningOfLine() 497 | editor.moveUp(2) 498 | }) 499 | } 500 | 501 | if (word != null ? word.length : undefined) { 502 | return this.model 503 | .load(uri) 504 | .filter( 505 | ({ responseType }: any) => 506 | responseType === 'return', 507 | ) 508 | .flatMap(() => this.model.makeWith(line + 1, word)) 509 | .subscribe(successHandler, this.displayErrors) 510 | } else { 511 | return this.clearMessagePanel( 512 | 'Idris: Illegal position to make a with view', 513 | ) 514 | } 515 | }) 516 | } 517 | } 518 | 519 | // construct a lemma from a hole 520 | doMakeLemma() { 521 | const editor = this.getEditor() 522 | if (editor) { 523 | return this.saveFile(editor).then((uri) => { 524 | let line = editor.getLastCursor().getBufferRow() 525 | const word = getWordUnderCursor(editor) 526 | this.clearMessagePanel('Idris: Make lemma ...') 527 | 528 | const successHandler = ({ msg }: any) => { 529 | // param1 contains the code which replaces the hole 530 | // param2 contains the code for the lemma function 531 | let [lemty, param1, param2] = msg 532 | param2 = this.prefixLiterateClause(param2) 533 | 534 | this.hideAndClearMessagePanel() 535 | 536 | return editor.transact(function () { 537 | if (lemty === ':metavariable-lemma') { 538 | // Move the cursor to the beginning of the word 539 | editor.moveToBeginningOfWord() 540 | // Because the ? in the Holes isn't part of 541 | // the word, we move left once, and then select two 542 | // words 543 | editor.moveLeft() 544 | editor.selectToEndOfWord() 545 | editor.selectToEndOfWord() 546 | // And then replace the replacement with the lemma call.. 547 | editor.insertText(param1[1]) 548 | 549 | // Now move to the previous blank line and insert the type 550 | // of the lemma 551 | editor.moveToBeginningOfLine() 552 | line = editor.getLastCursor().getBufferRow() 553 | 554 | // I tried to make this a function but failed to find out how 555 | // to call it and gave up... 556 | while (line > 0) { 557 | editor.moveToBeginningOfLine() 558 | editor.selectToEndOfLine() 559 | const contents = editor.getSelectedText() 560 | if (contents === '') { 561 | break 562 | } 563 | editor.moveUp() 564 | line-- 565 | } 566 | 567 | editor.insertNewlineBelow() 568 | editor.insertText(param2[1]) 569 | return editor.insertNewlineBelow() 570 | } 571 | }) 572 | } 573 | 574 | return this.model 575 | .load(uri) 576 | .filter( 577 | ({ responseType }: any) => responseType === 'return', 578 | ) 579 | .flatMap(() => this.model.makeLemma(line + 1, word)) 580 | .subscribe(successHandler, this.displayErrors) 581 | }) 582 | } 583 | } 584 | 585 | // create a case statement 586 | doMakeCase() { 587 | const editor = this.getEditor() 588 | if (editor) { 589 | return this.saveFile(editor).then((uri) => { 590 | const line = editor.getLastCursor().getBufferRow() 591 | const word = getWordUnderCursor(editor) 592 | this.clearMessagePanel('Idris: Make case ...') 593 | 594 | const successHandler = ({ msg }: any) => { 595 | const [clause] = Array.from(this.prefixLiterateClause(msg)) 596 | 597 | this.hideAndClearMessagePanel() 598 | 599 | return editor.transact(() => { 600 | // Delete old line, insert the new case block 601 | editor.moveToBeginningOfLine() 602 | editor.deleteLine() 603 | editor.insertText(clause) 604 | this.insertNewlineWithoutAutoIndent(editor) 605 | // And move the cursor to the beginning of 606 | // the new line 607 | editor.moveToBeginningOfLine() 608 | editor.moveUp(2) 609 | }) 610 | } 611 | 612 | return this.model 613 | .load(uri) 614 | .filter( 615 | ({ responseType }: any) => responseType === 'return', 616 | ) 617 | .flatMap(() => this.model.makeCase(line + 1, word)) 618 | .subscribe(successHandler, this.displayErrors) 619 | }) 620 | } 621 | } 622 | 623 | // show all holes in the current file 624 | showHoles() { 625 | const editor = this.getEditor() 626 | if (editor) { 627 | return this.saveFile(editor).then((uri) => { 628 | this.clearMessagePanel('Idris: Searching holes ...') 629 | 630 | const successHandler = ({ msg }: any) => { 631 | const [holes] = msg 632 | this.clearMessagePanel('Idris: Holes') 633 | const holesView = new HolesView() 634 | holesView.initialize(holes) 635 | this.messages.add(holesView) 636 | } 637 | 638 | return this.model 639 | .load(uri) 640 | .filter( 641 | ({ responseType }: any) => responseType === 'return', 642 | ) 643 | .flatMap(() => this.model.holes(80)) 644 | .subscribe(successHandler, this.displayErrors) 645 | }) 646 | } 647 | } 648 | 649 | /** 650 | * Replace a hole with a proof. 651 | */ 652 | doProofSearch() { 653 | const editor = this.getEditor() 654 | if (editor) { 655 | return this.saveFile(editor).then((uri) => { 656 | const line = editor.getLastCursor().getBufferRow() 657 | const word = getWordUnderCursor(editor) 658 | this.clearMessagePanel('Idris: Searching proof ...') 659 | 660 | const successHandler = ({ msg }: any) => { 661 | const [res] = msg 662 | 663 | this.hideAndClearMessagePanel() 664 | if (res.startsWith('?')) { 665 | // proof search returned a new hole 666 | this.clearMessagePanel( 667 | 'Idris: Searching proof was not successful.', 668 | ) 669 | } else { 670 | editor.transact(() => { 671 | // Move the cursor to the beginning of the word 672 | editor.moveToBeginningOfWord() 673 | // Because the ? in the Holes isn't part of 674 | // the word, we move left once, and then select two 675 | // words 676 | editor.moveLeft() 677 | editor.selectToEndOfWord() 678 | editor.selectToEndOfWord() 679 | // And then replace the replacement with the guess.. 680 | editor.insertText(res) 681 | }) 682 | } 683 | } 684 | 685 | return this.model 686 | .load(uri) 687 | .filter( 688 | ({ responseType }: any) => responseType === 'return', 689 | ) 690 | .flatMap(() => this.model.proofSearch(line + 1, word)) 691 | .subscribe(successHandler, this.displayErrors) 692 | }) 693 | } 694 | } 695 | 696 | doBrowseNamespace() { 697 | const editor = this.getEditor() 698 | if (editor) { 699 | return this.saveFile(editor).then((uri) => { 700 | let nameSpace = editor.getSelectedText() 701 | 702 | this.clearMessagePanel( 703 | 'Idris: Browsing namespace ' + nameSpace + '', 704 | ) 705 | 706 | const successHandler = ({ msg }: any) => { 707 | // the information is in a two dimensional array 708 | // one array contains the namespaces contained in the namespace 709 | // and the seconds all the methods 710 | const namesSpaceInformation = msg[0][0] 711 | for (nameSpace of namesSpaceInformation) { 712 | this.rawMessage(nameSpace) 713 | } 714 | 715 | const methodInformation = msg[0][1] 716 | return (() => { 717 | const result = [] 718 | for (let [ 719 | line, 720 | highlightInformation, 721 | ] of methodInformation) { 722 | const highlighting = highlighter.highlight( 723 | line, 724 | highlightInformation, 725 | ) 726 | const info = highlighter.highlightToString( 727 | highlighting, 728 | ) 729 | result.push(this.rawMessage(info)) 730 | } 731 | return result 732 | })() 733 | } 734 | 735 | return this.model 736 | .load(uri) 737 | .filter( 738 | ({ responseType }: any) => responseType === 'return', 739 | ) 740 | .flatMap(() => this.model.browseNamespace(nameSpace)) 741 | .subscribe(successHandler, this.displayErrors) 742 | }) 743 | } 744 | } 745 | 746 | /** 747 | * get the definition of a function or type 748 | */ 749 | printDefinition() { 750 | const editor = this.getEditor() 751 | if (editor) { 752 | return this.saveFile(editor).then((uri) => { 753 | const word = Symbol.serializeWord(getWordUnderCursor(editor)) 754 | this.clearMessagePanel( 755 | 'Idris: Searching definition of ' + word + ' ...', 756 | ) 757 | 758 | const successHandler = ({ msg }: any) => { 759 | const [type, highlightingInfo] = Array.from(msg) 760 | this.clearMessagePanel( 761 | 'Idris: Definition of ' + word + '', 762 | ) 763 | const informationView = new InformationView() 764 | informationView.initialize({ 765 | obligation: type, 766 | highlightingInfo, 767 | }) 768 | return this.messages.add(informationView) 769 | } 770 | 771 | return this.model 772 | .load(uri) 773 | .filter( 774 | ({ responseType }: any) => responseType === 'return', 775 | ) 776 | .flatMap(() => this.model.printDefinition(word)) 777 | .catch(() => this.model.printDefinition(word)) 778 | .subscribe(successHandler, this.displayErrors) 779 | }) 780 | } 781 | } 782 | 783 | /** 784 | * open the repl window 785 | */ 786 | openREPL() { 787 | const editor = this.getEditor() 788 | if (editor) { 789 | const uri = editor.getPath() as any 790 | this.clearMessagePanel('Idris: opening REPL ...') 791 | 792 | const successHandler = () => { 793 | this.hideAndClearMessagePanel() 794 | 795 | const options: WorkspaceOpenOptions = { 796 | split: 'right', 797 | searchAllPanes: true, 798 | } 799 | 800 | atom.workspace.open('idris://repl', options) 801 | } 802 | 803 | return this.model 804 | .load(uri) 805 | .filter(({ responseType }: any) => responseType === 'return') 806 | .subscribe(successHandler, this.displayErrors) 807 | } 808 | } 809 | 810 | /** 811 | * open the apropos window 812 | */ 813 | apropos() { 814 | const editor = this.getEditor() 815 | if (editor) { 816 | const uri = editor.getPath() as any 817 | this.clearMessagePanel('Idris: opening apropos view ...') 818 | 819 | const successHandler = () => { 820 | this.hideAndClearMessagePanel() 821 | 822 | const options: WorkspaceOpenOptions = { 823 | split: 'right', 824 | searchAllPanes: true, 825 | } 826 | 827 | return atom.workspace.open('idris://apropos', options) 828 | } 829 | 830 | return this.model 831 | .load(uri) 832 | .filter(({ responseType }: any) => responseType === 'return') 833 | .subscribe(successHandler, this.displayErrors) 834 | } 835 | } 836 | 837 | // generic function to display errors in the status bar 838 | displayErrors(err: any) { 839 | this.clearMessagePanel(' Idris Errors') 840 | 841 | // display the general error message 842 | if (err.message != null) { 843 | this.rawMessage(err.message) 844 | } 845 | 846 | return (() => { 847 | const result = [] 848 | for (let warning of err.warnings) { 849 | const type = warning[3] 850 | const highlightingInfo = warning[4] 851 | const highlighting = highlighter.highlight( 852 | type, 853 | highlightingInfo, 854 | ) 855 | const info = highlighter.highlightToString(highlighting) 856 | 857 | const line = warning[1][0] 858 | const character = warning[1][1] 859 | const uri = warning[0].replace('./', err.cwd + '/') 860 | 861 | // this provides information about the line and column of the error 862 | this.messages.add( 863 | new LineMessageView({ 864 | line, 865 | character, 866 | file: uri, 867 | }), 868 | ) 869 | 870 | // this provides a highlighted version of the error message 871 | // returned by idris 872 | this.rawMessage(info) 873 | 874 | const editor = atom.workspace.getActiveTextEditor() 875 | if (editor && line > 0 && uri === editor.getPath()) { 876 | const startPoint = warning[1] 877 | startPoint[0] = startPoint[0] - 1 878 | const endPoint = warning[2] 879 | endPoint[0] = endPoint[0] - 1 880 | const gutterMarker = this.createMarker( 881 | editor, 882 | [startPoint, endPoint], 883 | 'line-number', 884 | ) 885 | const lineMarker = this.createMarker( 886 | editor, 887 | [ 888 | [line - 1, character - 1], 889 | [line, 0], 890 | ], 891 | 'line', 892 | ) 893 | this.errorMarkers.push(gutterMarker) 894 | result.push(this.errorMarkers.push(lineMarker)) 895 | } else { 896 | result.push(undefined) 897 | } 898 | } 899 | return result 900 | })() 901 | } 902 | } 903 | --------------------------------------------------------------------------------