├── .gitignore ├── LICENSE.md ├── README.md ├── coffeelint.json ├── keymaps └── ocaml-merlin.cson ├── lib ├── buffer.coffee ├── main.coffee ├── merlin.coffee ├── rename-view.coffee ├── selection-view.coffee └── type-view.coffee ├── package.json └── styles └── ocaml-merlin.atom-text-editor.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Pieter Goetschalckx 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ocaml-merlin 2 | 3 | _Use [ocamlmerlin] to autocomplete, lint, refactor and navigate your OCaml/Reason code in Atom._ 4 | 5 | **Note: Although this package is still working, [ide-reason] provides better OCaml support.** 6 | 7 | 8 | ## Features 9 | 10 | * Context-sensitive autocompletion and linting 11 | * Show the type of expressions under the cursor 12 | * Find all occurrences of a variable 13 | * Jump to (type) declarations and back 14 | * Shrink and grow selections in a smart way 15 | * Rename all occurrences of a variable in a file 16 | * Destruct expressions in pattern matchings 17 | 18 | 19 | ## Usage 20 | 21 | Linting is performed on save or while typing by [linter]. Autocompletion is performed on the fly by [autocomplete-plus]. 22 | 23 | No default keybindings are provided, except those compatible with the `symbols-view` package. Additional keybindings can be set in your keymap. 24 | 25 | | Command | Description | Keybinding (Linux, Windows) | Keybinding (OS X) | 26 | | -------------------------------------- | -------------------------------------- | --------------------------- | ----------------------- | 27 | | `ocaml-merlin:toggle-type` | Toggle type of expression under cursor | | | 28 | | `ocaml-merlin:shrink-type` | Shrink the expression | | | 29 | | `ocaml-merlin:expand-type` | Expand the expression | | | 30 | | `ocaml-merlin:destruct` | Destruct expression under cursor | | | 31 | | `ocaml-merlin:next-occurrence` | Find next occurrence of expression | | | 32 | | `ocaml-merlin:previous-occurrence` | Find previous occurrence of expression | | | 33 | | `ocaml-merlin:go-to-declaration` | Go to declaration of expression | ctrl-alt-down | cmd-alt-down | 34 | | `ocaml-merlin:go-to-type-declaration` | Go to type declaration of expression | | | 35 | | `ocaml-merlin:return-from-declaration` | Go back to expression | ctrl-alt-up | cmd-alt-up | 36 | | `ocaml-merlin:shrink-selection` | Shrink the current selection | | | 37 | | `ocaml-merlin:expand-selection` | Expand the current selection | | | 38 | | `ocaml-merlin:rename-variable` | Rename all occurrences of variable | | | 39 | 40 | 41 | ## Installation 42 | 43 | This package requires [language-ocaml], [linter] and [ocamlmerlin]. For auto-indenting destructed patterns, [ocaml-indent] is needed. For Reason support, [language-reason] is needed, and [reason-refmt] is recommended. 44 | 45 | ```sh 46 | apm install language-ocaml linter ocaml-indent 47 | opam install merlin 48 | ``` 49 | 50 | [ocamlmerlin]: https://github.com/the-lambda-church/merlin 51 | [linter]: https://atom.io/packages/linter 52 | [autocomplete-plus]: https://atom.io/packages/autocomplete-plus 53 | [language-ocaml]: https://atom.io/packages/language-ocaml 54 | [ocaml-indent]: https://atom.io/packages/ocaml-indent 55 | [language-reason]: https://atom.io/packages/language-reason 56 | [reason-refmt]: https://atom.io/packages/reason-refmt 57 | [ide-reason]: https://atom.io/packages/ide-reason 58 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "level": "ignore" 4 | }, 5 | "no_empty_param_list": { 6 | "level": "error" 7 | }, 8 | "arrow_spacing": { 9 | "level": "error" 10 | }, 11 | "no_interpolation_in_single_quotes": { 12 | "level": "error" 13 | }, 14 | "no_debugger": { 15 | "level": "error" 16 | }, 17 | "prefer_english_operator": { 18 | "level": "error" 19 | }, 20 | "colon_assignment_spacing": { 21 | "spacing": { 22 | "left": 0, 23 | "right": 1 24 | }, 25 | "level": "error" 26 | }, 27 | "braces_spacing": { 28 | "spaces": 0, 29 | "level": "error" 30 | }, 31 | "spacing_after_comma": { 32 | "level": "error" 33 | }, 34 | "no_stand_alone_at": { 35 | "level": "error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /keymaps/ocaml-merlin.cson: -------------------------------------------------------------------------------- 1 | 'atom-text-editor[data-grammar="source ocaml"]': 2 | 'escape': 'ocaml-merlin:close-bubble' 3 | 4 | '.platform-linux atom-text-editor[data-grammar="source ocaml"]': 5 | 'ctrl-alt-down': 'ocaml-merlin:go-to-declaration' 6 | 'ctrl-alt-up': 'ocaml-merlin:return-from-declaration' 7 | 8 | '.platform-win32 atom-text-editor[data-grammar="source ocaml"]': 9 | 'ctrl-alt-down': 'ocaml-merlin:go-to-declaration' 10 | 'ctrl-alt-up': 'ocaml-merlin:return-from-declaration' 11 | 12 | '.platform-darwin atom-text-editor[data-grammar="source ocaml"]': 13 | 'cmd-alt-down': 'ocaml-merlin:go-to-declaration' 14 | 'cmd-alt-up': 'ocaml-merlin:return-from-declaration' 15 | -------------------------------------------------------------------------------- /lib/buffer.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | module.exports = class Buffer 4 | constructor: (@buffer, @destroyCallback) -> 5 | @subscriptions = new CompositeDisposable 6 | @subscriptions.add @buffer.onDidDestroy @destroyCallback 7 | @subscriptions.add @buffer.onDidChange => 8 | @changed = true 9 | @changed = true 10 | 11 | isChanged: -> @changed 12 | 13 | setChanged: (@changed) -> 14 | 15 | getPath: -> @buffer.getPath() 16 | 17 | getText: -> @buffer.getText() 18 | 19 | onDidDestroy: (callback) -> 20 | @subscriptions.add @buffer.onDidDestroy callback 21 | 22 | onDidChange: (callback) -> 23 | @subscriptions.add @buffer.onDidChange callback 24 | 25 | destroy: -> 26 | @subscriptions.dispose() 27 | @destroyCallback() 28 | -------------------------------------------------------------------------------- /lib/main.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Disposable} = require 'atom' 2 | 3 | languages = ['ocaml', 'ocamllex', 'ocamlyacc', 'reason'] 4 | scopes = ['ocaml'].concat languages.map (language) -> "source.#{language}" 5 | selectors = languages.map (language) -> ".source.#{language}" 6 | 7 | Merlin = null 8 | Buffer = null 9 | TypeView = null 10 | SelectionView = null 11 | RenameView = null 12 | 13 | module.exports = 14 | merlin: null 15 | subscriptions: null 16 | buffers: {} 17 | 18 | typeViews: {} 19 | selectionViews: {} 20 | 21 | latestType: null 22 | 23 | occurrences: null 24 | 25 | positions: [] 26 | 27 | indentRange: null 28 | 29 | activate: (state) -> 30 | Merlin = require './merlin' 31 | Buffer = require './buffer' 32 | TypeView = require './type-view' 33 | SelectionView = require './selection-view' 34 | 35 | @merlin = new Merlin 36 | 37 | @subscriptions = new CompositeDisposable 38 | 39 | @subscriptions.add atom.config.onDidChange 'ocaml-merlin.merlinPath', => 40 | @restartMerlin() 41 | 42 | target = scopes.map (scope) -> 43 | "atom-text-editor[data-grammar='#{scope.replace /\./g, ' '}']" 44 | .join ', ' 45 | 46 | @subscriptions.add atom.commands.add target, 47 | 'ocaml-merlin:show-type': => @toggleType() 48 | 'ocaml-merlin:toggle-type': => @toggleType() 49 | 'ocaml-merlin:shrink-type': => @shrinkType() 50 | 'ocaml-merlin:expand-type': => @expandType() 51 | 'ocaml-merlin:close-bubble': => @closeType() 52 | 'ocaml-merlin:insert-latest-type': => @insertType() 53 | 'ocaml-merlin:destruct': => @destruct() 54 | 'ocaml-merlin:next-occurrence': => @getOccurrence(1) 55 | 'ocaml-merlin:previous-occurrence': => @getOccurrence(-1) 56 | 'ocaml-merlin:go-to-declaration': => @goToDeclaration('ml') 57 | 'ocaml-merlin:go-to-type-declaration': => @goToDeclaration('mli') 58 | 'ocaml-merlin:return-from-declaration': => @returnFromDeclaration() 59 | 'ocaml-merlin:shrink-selection': => @shrinkSelection() 60 | 'ocaml-merlin:expand-selection': => @expandSelection() 61 | 'ocaml-merlin:rename-variable': => @renameVariable() 62 | 'ocaml-merlin:restart-merlin': => @restartMerlin() 63 | 64 | @subscriptions.add atom.workspace.observeTextEditors (editor) => 65 | @subscriptions.add editor.observeGrammar (grammar) => 66 | if scopes.includes grammar.scopeName 67 | @addBuffer editor.getBuffer() 68 | else 69 | @removeBuffer editor.getBuffer() 70 | @subscriptions.add editor.onDidDestroy => 71 | delete @typeViews[editor.id] 72 | delete @selectionViews[editor.id] 73 | 74 | restartMerlin: -> 75 | buffer.setChanged true for _, buffer of @buffers 76 | @merlin.restart() 77 | 78 | addBuffer: (textBuffer) -> 79 | bufferId = textBuffer.getId() 80 | return if @buffers[bufferId]? 81 | buffer = new Buffer textBuffer, => delete @buffers[bufferId] 82 | @buffers[bufferId] = buffer 83 | @merlin.project buffer 84 | .then ({merlinFiles, failures}) => 85 | atom.workspace.notificationManager.addError failures.join '\n' if failures? 86 | return if merlinFiles.length 87 | @merlin.setFlags buffer, atom.config.get 'ocaml-merlin.default.flags' 88 | .then ({failures}) -> 89 | atom.workspace.notificationManager.addError failures.join '\n' if failures? 90 | @merlin.usePackages buffer, atom.config.get 'ocaml-merlin.default.packages' 91 | .then ({failures}) -> 92 | atom.workspace.notificationManager.addError failures.join '\n' if failures? 93 | @merlin.enableExtensions buffer, atom.config.get 'ocaml-merlin.default.extensions' 94 | @merlin.addSourcePaths buffer, atom.config.get 'ocaml-merlin.default.sourcePaths' 95 | @merlin.addBuildPaths buffer, atom.config.get 'ocaml-merlin.default.buildPaths' 96 | 97 | removeBuffer: (textBuffer) -> 98 | @buffers[textBuffer.getId()]?.destroy() 99 | 100 | getBuffer: (editor) -> 101 | textBuffer = editor.getBuffer() 102 | buffer = @buffers[textBuffer.getId()] 103 | return buffer if buffer? 104 | @addBuffer textBuffer 105 | @buffers[textBuffer.getId()] 106 | 107 | toggleType: -> 108 | return unless editor = atom.workspace.getActiveTextEditor() 109 | if @typeViews[editor.id]?.marker?.isValid() 110 | @typeViews[editor.id].destroy() 111 | delete @typeViews[editor.id] 112 | else 113 | @merlin.type @getBuffer(editor), editor.getCursorBufferPosition() 114 | .then (typeList) => 115 | return unless typeList.length 116 | typeView = new TypeView typeList, editor 117 | @latestType = typeView.show() 118 | @typeViews[editor.id] = typeView 119 | 120 | shrinkType: -> 121 | return unless editor = atom.workspace.getActiveTextEditor() 122 | @latestType = @typeViews[editor.id]?.shrink() 123 | 124 | expandType: -> 125 | return unless editor = atom.workspace.getActiveTextEditor() 126 | @latestType = @typeViews[editor.id]?.expand() 127 | 128 | closeType: -> 129 | return unless editor = atom.workspace.getActiveTextEditor() 130 | @typeViews[editor.id]?.destroy() 131 | delete @typeViews[editor.id] 132 | 133 | insertType: -> 134 | return unless @latestType? 135 | return unless editor = atom.workspace.getActiveTextEditor() 136 | editor.insertText @latestType 137 | 138 | destruct: -> 139 | return unless editor = atom.workspace.getActiveTextEditor() 140 | @merlin.destruct @getBuffer(editor), editor.getSelectedBufferRange() 141 | .then ({range, content}) => 142 | editor.transact 100, => 143 | range = editor.setTextInBufferRange range, content 144 | @indentRange editor, range if @indentRange? 145 | , ({message}) -> 146 | atom.workspace.notificationManager.addError message 147 | 148 | getOccurrence: (offset) -> 149 | return unless editor = atom.workspace.getActiveTextEditor() 150 | point = editor.getCursorBufferPosition() 151 | @merlin.occurrences @getBuffer(editor), point 152 | .then (ranges) -> 153 | index = ranges.findIndex (range) -> range.containsPoint point 154 | range = ranges[(index + offset) % ranges.length] 155 | editor.setSelectedBufferRange range 156 | 157 | goToDeclaration: (kind) -> 158 | return unless editor = atom.workspace.getActiveTextEditor() 159 | currentPoint = editor.getCursorBufferPosition() 160 | @merlin.locate @getBuffer(editor), currentPoint, kind 161 | .then ({file, point}) => 162 | @positions.push 163 | file: editor.getPath() 164 | point: currentPoint 165 | if file isnt editor.getPath() 166 | atom.workspace.open file, 167 | initialLine: point.row 168 | initialColumn: point.column 169 | pending: true 170 | searchAllPanes: true 171 | else 172 | editor.setCursorBufferPosition point 173 | , (reason) -> 174 | atom.workspace.notificationManager.addError reason 175 | 176 | returnFromDeclaration: -> 177 | return unless position = @positions.pop() 178 | atom.workspace.open position.file, 179 | initialLine: position.point.row 180 | initialColumn: position.point.column 181 | pending: true 182 | searchAllPanes: true 183 | 184 | getSelectionView: -> 185 | return unless editor = atom.workspace.getActiveTextEditor() 186 | selectionView = @selectionViews[editor.id] 187 | return Promise.resolve(selectionView) if selectionView?.isAlive() 188 | @merlin.enclosing @getBuffer(editor), editor.getCursorBufferPosition() 189 | .then (ranges) => 190 | selectionView = new SelectionView editor, ranges 191 | @selectionViews[editor.id] = selectionView 192 | 193 | shrinkSelection: -> 194 | @getSelectionView().then (selectionView) -> selectionView.shrink() 195 | 196 | expandSelection: -> 197 | @getSelectionView().then (selectionView) -> selectionView.expand() 198 | 199 | renameView: (name, callback) -> 200 | RenameView ?= require './rename-view' 201 | new RenameView {name, callback} 202 | 203 | renameVariable: -> 204 | return unless editor = atom.workspace.getActiveTextEditor() 205 | @merlin.occurrences @getBuffer(editor), editor.getCursorBufferPosition() 206 | .then (ranges) => 207 | currentName = editor.getTextInBufferRange ranges[0] 208 | @renameView currentName, (newName) -> 209 | editor.transact -> 210 | ranges.reverse().map (range) -> 211 | editor.setTextInBufferRange range, newName 212 | 213 | deactivate: -> 214 | @merlin.close() 215 | @subscriptions.dispose() 216 | buffer.destroy() for _, buffer of @buffers 217 | 218 | getPrefix: (editor, point) -> 219 | line = editor.getTextInBufferRange([[point.row, 0], point]) 220 | line.match(/[^\s\[\](){}<>,+*\/-]*$/)[0] 221 | 222 | provideAutocomplete: -> 223 | minimumWordLength = 1 224 | @subscriptions.add atom.config.observe "autocomplete-plus.minimumWordLength", (value) -> 225 | minimumWordLength = value 226 | completePartialPrefixes = false 227 | @subscriptions.add atom.config.observe "ocaml-merlin.completePartialPrefixes", (value) -> 228 | completePartialPrefixes = value 229 | kindToType = 230 | "Value": "value" 231 | "Variant": "variable" 232 | "Constructor": "class" 233 | "Label": "keyword" 234 | "Module": "method" 235 | "Signature": "type" 236 | "Type": "type" 237 | "Method": "property" 238 | "#": "constant" 239 | "Exn": "keyword" 240 | "Class": "class" 241 | selector: selectors.join ', ' 242 | getSuggestions: ({editor, bufferPosition, activatedManually}) => 243 | prefix = @getPrefix editor, bufferPosition 244 | return [] if prefix.length < (if activatedManually then 1 else minimumWordLength) 245 | if completePartialPrefixes 246 | replacement = prefix 247 | promise = @merlin.expand @getBuffer(editor), bufferPosition, prefix 248 | else 249 | index = prefix.lastIndexOf "." 250 | replacement = prefix.substr(index + 1) if index >= 0 251 | promise = @merlin.complete @getBuffer(editor), bufferPosition, prefix 252 | promise.then (entries) -> 253 | entries.map ({name, kind, desc, info}) -> 254 | text: name 255 | replacementPrefix: replacement 256 | type: kindToType[kind] 257 | leftLabel: kind 258 | rightLabel: desc 259 | description: if info.length then info else desc 260 | disableForSelector: (selectors.map (selector) -> selector + " .comment").join ', ' 261 | inclusionPriority: 1 262 | 263 | provideLinter: -> 264 | name: 'OCaml Merlin' 265 | scope: 'file' 266 | lintsOnChange: atom.config.get 'ocaml-merlin.lintAsYouType' 267 | grammarScopes: scopes 268 | lint: (editor) => 269 | return [] unless buffer = @getBuffer(editor) 270 | @merlin.errors buffer 271 | .then (errors) -> 272 | errors.map ({range, type, message}) -> 273 | location: 274 | file: editor.getPath() 275 | position: range 276 | excerpt: if message.match '\n' then type[0].toUpperCase() + type[1..-1] else message 277 | severity: if type is 'warning' then 'warning' else 'error' 278 | description: if message.match '\n' then "```\n#{message}```" else null 279 | solutions: if m = message.match /Hint: Did you mean (.*)\?/ then [ 280 | position: range 281 | replaceWith: m[1] 282 | ] else [] 283 | 284 | consumeIndent: ({@indentRange}) -> 285 | new Disposable => @indentRange = null 286 | -------------------------------------------------------------------------------- /lib/merlin.coffee: -------------------------------------------------------------------------------- 1 | {spawn} = require 'child_process' 2 | {createInterface} = require 'readline' 3 | {Point, Range} = require 'atom' 4 | 5 | module.exports = class Merlin 6 | process: null 7 | interface: null 8 | protocol: 2 9 | 10 | queue: null 11 | 12 | constructor: -> 13 | @queue = Promise.resolve() 14 | @restart() 15 | 16 | restart: -> 17 | path = atom.config.get 'ocaml-merlin.merlinPath' 18 | @interface?.close() 19 | @process?.kill() 20 | 21 | projectPaths = atom.project.getPaths() 22 | @process = spawn path, [], cwd: if projectPaths.length > 0 then projectPaths[0] else __dirname 23 | @process.on 'error', (error) -> console.log error 24 | @process.on 'exit', (code) -> console.log "Merlin exited with code #{code}" 25 | console.log "Merlin process started, pid = #{@process.pid}" 26 | @interface = createInterface 27 | input: @process.stdout 28 | output: @process.stdin 29 | terminal: false 30 | @rawQuery ["protocol", "version", 2] 31 | .then ([kind, payload]) => 32 | @protocol = if kind is "return" then payload.selected else 1 33 | 34 | close: -> 35 | @interface?.close() 36 | @process?.kill() 37 | 38 | rawQuery: (query) -> 39 | @queue = @queue.then => 40 | new Promise (resolve, reject) => 41 | jsonQuery = JSON.stringify query 42 | @interface.question jsonQuery + '\n', (answer) -> 43 | resolve JSON.parse(answer) 44 | 45 | query: (buffer, query) -> 46 | @rawQuery 47 | context: ["auto", buffer.getPath()] 48 | query: query 49 | .then ([kind, payload]) -> 50 | new Promise (resolve, reject) -> 51 | if kind is "return" 52 | resolve payload 53 | else if kind is "error" 54 | reject payload.message 55 | else 56 | reject payload 57 | 58 | position: (point) -> 59 | point = Point.fromObject point 60 | {line: point.row + 1, col: point.column} 61 | 62 | point: (position) -> 63 | new Point position.line - 1, position.col 64 | 65 | range: (start, end) -> 66 | new Range (@point start), (@point end) 67 | 68 | sync: (buffer) -> 69 | return Promise.resolve(true) unless buffer.isChanged() 70 | buffer.setChanged false 71 | if @protocol is 1 72 | @query buffer, ["tell", "start", "at", @position([0, 0])] 73 | .then => @query buffer, ["tell", "source-eof", buffer.getText()] 74 | else if @protocol is 2 75 | @query buffer, ["tell", "start", "end", buffer.getText()] 76 | 77 | setFlags: (buffer, flags) -> 78 | @query buffer, ["flags", "set", flags] 79 | .then ({failures}) -> 80 | failures ? [] 81 | 82 | usePackages: (buffer, packages) -> 83 | @query buffer, ["find", "use", packages] 84 | .then ({failures}) -> 85 | failures ? [] 86 | 87 | listPackages: (buffer) -> 88 | @query buffer, ["find", "list"] 89 | 90 | enableExtensions: (buffer, extensions) -> 91 | @query buffer, ["extension", "enable", extensions] 92 | 93 | listExtensions: (buffer) -> 94 | @query buffer, ["extension", "list"] 95 | 96 | addSourcePaths: (buffer, paths) -> 97 | @query buffer, ["path", "add", "source", paths] 98 | 99 | addBuildPaths: (buffer, paths) -> 100 | @query buffer, ["path", "add", "build", paths] 101 | 102 | type: (buffer, point) -> 103 | @sync(buffer).then => 104 | @query buffer, ["type", "enclosing", "at", @position point] 105 | .then (types) => 106 | types.map ({start, end, type, tail}) => 107 | range: @range start, end 108 | type: type 109 | tail: tail 110 | 111 | destruct: (buffer, range) -> 112 | @sync(buffer).then => 113 | @query buffer, ["case", "analysis", "from", @position(range.start), 114 | "to", @position(range.end)] 115 | .then ([{start, end}, content]) => 116 | range: @range start, end 117 | content: content 118 | 119 | complete: (buffer, point, prefix) -> 120 | @sync(buffer).then => 121 | @query buffer, ["complete", "prefix", prefix, 122 | "at", @position point, "with", "doc"] 123 | .then ({entries}) -> 124 | entries 125 | 126 | expand: (buffer, point, prefix) -> 127 | @sync(buffer).then => 128 | @query buffer, ["expand", "prefix", prefix, 129 | "at", @position point] 130 | .then ({entries}) -> 131 | entries 132 | 133 | occurrences: (buffer, point) -> 134 | @sync(buffer).then => 135 | @query buffer, ["occurrences", "ident", "at", @position point] 136 | .then (occurrences) => 137 | occurrences.map ({start, end}) => 138 | @range start, end 139 | 140 | locate: (buffer, point, kind = 'ml') -> 141 | @sync(buffer).then => 142 | @query buffer, ["locate", null, kind, "at", @position point] 143 | .then (result) => 144 | new Promise (resolve, reject) => 145 | if typeof result is 'string' 146 | reject result 147 | else 148 | resolve 149 | file: result.file 150 | point: @point result.pos 151 | 152 | enclosing: (buffer, point) -> 153 | @sync(buffer).then => 154 | @query buffer, ["enclosing", @position point] 155 | .then (selections) => 156 | selections.map ({start, end}) => 157 | @range start, end 158 | 159 | errors: (buffer) -> 160 | @sync(buffer).then => 161 | @query buffer, ["errors"] 162 | .then (errors) => 163 | errors.map ({start, end, type, message}) => 164 | lines = message.split '\n' 165 | lines[0] = lines[0][0].toUpperCase() + lines[0][1..-1] 166 | if lines.length > 1 167 | indent = lines[1..-1].reduce (indent, line) -> 168 | Math.min indent, line.search /\S|$/ 169 | , Infinity 170 | for i in [1..lines.length-1] 171 | lines[i] = lines[i][indent..-1] 172 | range: if start? and end? then @range start, end else [[0, 0], [0, 0]] 173 | type: type 174 | message: lines.join '\n' 175 | 176 | project: (buffer) -> 177 | @query buffer, ["project", "get"] 178 | .then ({result, failures}) -> 179 | merlinFiles: result 180 | failures: failures 181 | -------------------------------------------------------------------------------- /lib/rename-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Disposable, TextEditor} = require 'atom' 2 | etch = require 'etch' 3 | $ = etch.dom 4 | 5 | module.exports = class RenameView 6 | constructor: ({@name, callback}) -> 7 | etch.initialize this 8 | @disposables = new CompositeDisposable 9 | @disposables.add atom.commands.add @element, 10 | 'core:confirm': => 11 | callback @refs.editor.getText() 12 | @destroy() 13 | 'core:cancel': => 14 | @destroy() 15 | handler = => 16 | @destroy() if document.hasFocus() 17 | @refs.editor.element.addEventListener 'blur', handler 18 | @disposables.add new Disposable => 19 | @refs.editor.element.removeEventListener 'blur', handler 20 | @panel = atom.workspace.addModalPanel item: @element 21 | @refs.editor.setText @name 22 | @refs.editor.selectAll() 23 | @refs.editor.element.focus() 24 | 25 | render: -> 26 | $.div 27 | class: 'ocaml-merlin-dialog', 28 | $.label 29 | class: 'icon icon-arrow-right', 30 | "Enter the new name for #{@name}.", 31 | $ TextEditor, 32 | ref: 'editor' 33 | mini: true 34 | 35 | update: -> 36 | etch.update this 37 | 38 | destroy: -> 39 | @disposables.dispose() 40 | etch.destroy this 41 | .then => 42 | @panel.destroy() 43 | atom.workspace.getActivePane().activate() 44 | -------------------------------------------------------------------------------- /lib/selection-view.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class SelectionView 2 | @editor: null 3 | 4 | @rangeList: null 5 | @rangeIndex: null 6 | 7 | @alive: null 8 | @subscription: null 9 | 10 | constructor: (@editor, @rangeList) -> 11 | @alive = true 12 | currentRange = @editor.getSelectedBufferRange() 13 | @rangeIndex = rangeList.findIndex (range) -> 14 | range.containsRange currentRange 15 | if not @rangeList[@rangeIndex].isEqual currentRange 16 | @rangeIndex -= 0.5 17 | @subscription = 18 | @editor.onDidChangeSelectionRange ({newBufferRange}) => 19 | return if newBufferRange.isEqual @rangeList[@rangeIndex] 20 | @rangeIndex = @rangeList.findIndex (range) -> 21 | range.isEqual(newBufferRange) 22 | @destroy() if @rangeIndex is -1 23 | 24 | expand: -> 25 | newIndex = Math.floor @rangeIndex + 1 26 | return unless newIndex < @rangeList?.length ? 0 27 | @rangeIndex = newIndex 28 | @editor.setSelectedBufferRange @rangeList[@rangeIndex] 29 | 30 | shrink: -> 31 | newIndex = Math.ceil @rangeIndex - 1 32 | return unless newIndex >= 0 33 | @rangeIndex = newIndex 34 | @editor.setSelectedBufferRange @rangeList[@rangeIndex] 35 | 36 | isAlive: -> 37 | @alive 38 | 39 | destroy: -> 40 | @alive = false 41 | @subscription?.dispose() 42 | -------------------------------------------------------------------------------- /lib/type-view.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | etch = require 'etch' 3 | $ = etch.dom 4 | 5 | module.exports = class TypeView 6 | @typeList: null 7 | @typeIndex: null 8 | 9 | @editor: null 10 | @marker: null 11 | 12 | @subscription: null 13 | 14 | constructor: (@typeList, @editor) -> 15 | @typeIndex = 0 16 | etch.initialize this 17 | @refs.editor.element.removeAttribute 'tabindex' 18 | 19 | render: -> 20 | $.div 21 | class: 'ocaml-merlin-type', 22 | $ TextEditor, 23 | ref: 'editor' 24 | mini: true 25 | grammar: atom.workspace.grammarRegistry.grammarForScopeName 'source.ocaml' 26 | autoHeight: true 27 | autoWidth: true 28 | 29 | update: -> 30 | etch.update this 31 | 32 | show: -> 33 | @destroy() 34 | {range, type} = @typeList[@typeIndex] 35 | @refs.editor.setText type 36 | @marker = @editor.markBufferRange range 37 | @editor.decorateMarker @marker, 38 | if range.isSingleLine() and type.split('\n').length < 10 39 | type: 'overlay' 40 | item: @element 41 | position: 'tail' 42 | class: 'ocaml-merlin' 43 | else 44 | type: 'block' 45 | item: @element 46 | position: 'after' 47 | @editor.decorateMarker @marker, 48 | type: 'highlight' 49 | class: 'ocaml-merlin' 50 | @subscription = @editor.onDidChangeCursorPosition => @destroy() 51 | type 52 | 53 | expand: -> 54 | return unless @typeIndex + 1 < @typeList?.length ? 0 55 | @typeIndex += 1 56 | @show() 57 | 58 | shrink: -> 59 | return unless @typeIndex > 0 60 | @typeIndex -= 1 61 | @show() 62 | 63 | destroy: -> 64 | # etch.destroy this 65 | # .then => 66 | @marker?.destroy() 67 | @subscription?.dispose() 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ocaml-merlin", 3 | "main": "./lib/main", 4 | "version": "0.14.7", 5 | "description": "Linting, autocompletion, type checking, refactoring and code navigation for Ocaml/Reason with Merlin.", 6 | "keywords": [ 7 | "linter", 8 | "autocomplete", 9 | "goto", 10 | "ocaml", 11 | "merlin" 12 | ], 13 | "activationHooks": [ 14 | "language-ocaml:grammar-used", 15 | "language-ocaml-fix:grammar-used", 16 | "language-reason:grammar-used", 17 | "nuclide-language-reason:grammar-used" 18 | ], 19 | "repository": "https://github.com/atom-ocaml/ocaml-merlin", 20 | "license": "MIT", 21 | "engines": { 22 | "atom": ">=1.14.0 <2.0.0" 23 | }, 24 | "dependencies": { 25 | "etch": "^0.9.5" 26 | }, 27 | "providedServices": { 28 | "linter": { 29 | "versions": { 30 | "2.0.0": "provideLinter" 31 | } 32 | }, 33 | "autocomplete.provider": { 34 | "versions": { 35 | "2.0.0": "provideAutocomplete" 36 | } 37 | } 38 | }, 39 | "consumedServices": { 40 | "ocamlIndent": { 41 | "versions": { 42 | "^0.3.0": "consumeIndent" 43 | } 44 | } 45 | }, 46 | "configSchema": { 47 | "merlinPath": { 48 | "type": "string", 49 | "default": "ocamlmerlin", 50 | "order": 1 51 | }, 52 | "lintAsYouType": { 53 | "type": "boolean", 54 | "default": true, 55 | "order": 2 56 | }, 57 | "completePartialPrefixes": { 58 | "description": "For instance, `L.ma` can get expanded to `List.map`.", 59 | "type": "boolean", 60 | "default": false, 61 | "order": 3 62 | }, 63 | "default": { 64 | "title": "Default settings if no .merlin file is found", 65 | "type": "object", 66 | "order": 4, 67 | "properties": { 68 | "flags": { 69 | "title": "Flags", 70 | "type": "array", 71 | "default": [], 72 | "order": 1, 73 | "items": { 74 | "type": "string" 75 | } 76 | }, 77 | "packages": { 78 | "title": "Packages", 79 | "type": "array", 80 | "default": [], 81 | "order": 2, 82 | "items": { 83 | "type": "string" 84 | } 85 | }, 86 | "extensions": { 87 | "title": "Extensions", 88 | "type": "array", 89 | "default": [], 90 | "order": 3, 91 | "items": { 92 | "type": "string" 93 | } 94 | }, 95 | "sourcePaths": { 96 | "title": "Source Paths", 97 | "type": "array", 98 | "default": [], 99 | "order": 4, 100 | "items": { 101 | "type": "string" 102 | } 103 | }, 104 | "buildPaths": { 105 | "title": "Build Paths", 106 | "type": "array", 107 | "default": [], 108 | "order": 5, 109 | "items": { 110 | "type": "string" 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /styles/ocaml-merlin.atom-text-editor.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | .highlight.ocaml-merlin .region { 4 | border-bottom: 1px solid @background-color-info; 5 | } 6 | 7 | atom-overlay.ocaml-merlin { 8 | z-index: 100; 9 | } 10 | 11 | .ocaml-merlin-type { 12 | background-color: @overlay-background-color; 13 | border-left: 0.5rem solid @overlay-border-color; 14 | border-radius: @component-border-radius; 15 | box-shadow: 2px 2px 2px darken(@overlay-background-color, 5%); 16 | 17 | atom-text-editor { 18 | box-sizing: content-box; 19 | padding: 0.75rem !important; 20 | box-shadow: none !important; 21 | border: none !important; 22 | font-size: inherit !important; 23 | background-color: @overlay-background-color !important; 24 | max-height: none !important; 25 | height: auto !important; 26 | } 27 | 28 | .cursors { 29 | display: none; 30 | } 31 | } 32 | --------------------------------------------------------------------------------