├── .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 |
--------------------------------------------------------------------------------