diff --git a/LICENSE.md b/LICENSE.md index 9ddc2d8..0ee0e7d 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2015 Ian Hattendorf +Copyright (c) 2015 Krzysztof Wende Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 4fded22..b6cf2ef 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,68 @@ +# PROJECT DEPRECATED BECAUSE OF DRASTIC CHANGES IN ATOM EXTERNAL PROCESS API. +# For Elixir autocompletion in Atom head to https://github.com/msaraiva/atom-elixir + + # Atom intelligent Elixir Autocompletion for Autocomplete+ +![Image of autocomplete-elixir](https://raw.githubusercontent.com/wende/autocomplete-elixir/master/pics/presentation.png) + ## Features - Intelligent autocompletion of - Global modules and functions - Local project modules and functions (those which compile successfully) - Type hints for - - Arguments + - Arguments - Return types +- Type aliases replaced with primitive structures they represent - Snippets for common structures - -## Incoming features -- Local variables autcompletion -- Variable type inference (by priority) - 1. Assignment ( T = T ) - 2. Expressions ( T = fn() :: T , T = T + T) - 3. Extraction ( [ T | [T] ] = [T] ) - 4. Matching ( { T1, T2 } = {T1, T2} ) - 5. Remote types -- Obvious type errors warnings ( Variable doesn't conform to required type / Extraction of non-parametric type) -- Feel free to suggest additional features at [issues page](https://github.com/iraasta/autocomplete-elixir/issues) - +- `do`/`fn` -> `end` highlighting +- Jump to local function/macro defintion with `alt-.` and back with `alt-,` ## Installation Installation is done using Atom package manager or command apm install autocomplete-elixir +CAUTION: MAKE SURE TO HAVE `autocomplete-plus` PACKAGE INSTALLED + + +## Incoming features +### 1.6 +- Jump to definition out of local module + +Feel free to suggest additional features at [issues page](https://github.com/iraasta/autocomplete-elixir/issues) + +### Common Errors + +#### Package spits out a lot of errors on my OSX + It seems that OSX has a lot of different safe measures which don't cooperate nicely with atom environment. + Make sure you've got both erlang and elixir installed and paths set up in package settings: + ![Image of autocomplete-elixir](https://raw.githubusercontent.com/wende/autocomplete-elixir/master/pics/Screen.Shot.2016-02-19.at.17.12.58.png) + + + For optimal behaviour always start atom from command line instead of Finder. + +#### `Failed to spawn command elixir. Make sure elixir is installed and in your PATH` + Let me guess. You're using OSX. This happens when starting atom from Finder. + Finder-started applications have no access to PATH variable. To go around that make + sure to set "Elixir Path" in package configuration to Your absolute elixir executable + path or start atom from command line instead. + + ### Required modules - [autocomplete+](https://atom.io/packages/autocomplete-plus) - [autocomplete-snippets](https://atom.io/packages/autocomplete-snippets) - [language-elixir](https://atom.io/packages/language-elixir) -### Recommended modules -- [term](https://atom.io/packages/term) -- [layout-manager](https://atom.io/packages/layout-manager) +### Troubleshooting +1. Make sure you've got both Elixir and Erlang installed +2. Make sure you've got both paths set up in settings +You can check both things by running: +`which elixir` -> /usr/local/bin/elixir +`which erl` -> /usr/local/bin/erl +And insert the whole path of elixir but only folder path of erl +![Image of autocomplete-elixir](https://raw.githubusercontent.com/wende/autocomplete-elixir/master/pics/Screen.Shot.2016-02-19.at.17.12.58.png) +3. Make sure You've got Elixir-language package installed +4. Try running atom from the CLI +5. If functions are not showing up in the auto-complete list, be sure that atom's root directory has your `mix.exs` file. This can be either your individual application or an umbrella app. +6. Read existing issues ;) diff --git a/keymaps/autocomplete-elixir.cson b/keymaps/autocomplete-elixir.cson new file mode 100644 index 0000000..5725dff --- /dev/null +++ b/keymaps/autocomplete-elixir.cson @@ -0,0 +1,3 @@ +'atom-text-editor': + "alt-,": 'autocomplete-elixir:backward' + "alt-.": 'autocomplete-elixir:jump-to-definition' diff --git a/lib/alchemide/autocompleter/LICENSE.md b/lib/alchemide/autocompleter/LICENSE.md new file mode 100644 index 0000000..8f7f102 --- /dev/null +++ b/lib/alchemide/autocompleter/LICENSE.md @@ -0,0 +1,23 @@ +Copyright (c) 2015, Krzysztof Wende +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/alchemide/autocompleter/README.md b/lib/alchemide/autocompleter/README.md new file mode 100644 index 0000000..041ca66 --- /dev/null +++ b/lib/alchemide/autocompleter/README.md @@ -0,0 +1,25 @@ +# Autocomplete-elixir's heart + +## Running + +To run the script use +`elixir autocomplete.exs ` +Where paths are root directories of files that are being used during development. + +### COMMANDS +Each command's format is: +`command param param2\n` + +### Autocomplete +- `a #{prefix}` where prefix is a word currently beign typed in ex: `Enum.m` +- `ea #{prefix}` Same but for Erlang. +- `l #{path}` load a file to make it's modules available for autocompletion (Suggested after file saving) + +#### Format of output + +`exists<>one<>multi1;multi2;multi3` + +Where: +- `exists` is a boolean +- `one` is a oneliner suggestion that continues the typed word (f.i for word `Syst` suggestion will be `em`) +- `multi` when there is more than one suggestion (f.i for word `Enum.a` it's `all?/2;any?/2;at/3`) diff --git a/lib/alchemide/autocompleter/autocomplete.exs b/lib/alchemide/autocompleter/autocomplete.exs index 9b46572..7ee342d 100644 --- a/lib/alchemide/autocompleter/autocomplete.exs +++ b/lib/alchemide/autocompleter/autocomplete.exs @@ -1,3 +1,5 @@ +Application.put_env(:iex, :autocomplete_server, IEx.Server) + spec_to_ast = fn a,b -> Kernel.Typespec.spec_to_ast(a,b) end y = fn f -> @@ -14,24 +16,20 @@ y2 = fn f -> fun.(fun) end - replaceTypes = fn replaceType -> fn [{type, line, name, args} | types], typesMap -> - IO.puts "replacing" case typesMap[name] do nil -> [{type, line, name, replaceType.(args,typesMap)} | replaceType.(types,typesMap)] type -> [type | replaceType.(types,typesMap)] end - #[{type, line, name, replaceType.(args, typesMap)} | replaceType.(types, typesMap)] [type | types], typesMap -> [type | replaceType.(types, typesMap)] [], _ -> [] + any, _ -> any end end replaceTypes = y2.(replaceTypes) - - zipFunSpec = fn (a, nil) -> a (a, []) -> a @@ -56,7 +54,7 @@ end getSpec = fn module -> type_aliases = (Kernel.Typespec.beam_types(module) || []) |> Enum.reduce %{}, fn - ({:type, {name, type, arg}},b) -> Map.put(b, name, type) + ({_, {name, type, arg}},b) -> Map.put(b, name, type) end (Kernel.Typespec.beam_specs(module) || []) @@ -64,18 +62,18 @@ getSpec = fn module -> |> Enum.reduce(%{}, reducer) end -pairWithSpec = fn input, fns -> - case String.contains?(input, ".") do +pairWithSpec = fn +input, fns, regex -> + case Regex.match?(~r/\.|:/, input) do true -> - mod = Regex.replace ~r/\.\w*$/, input, "" + mod = Regex.replace regex, input, "" {atom, _} = Code.eval_string(mod) specMap = getSpec.(atom) re = ~r/\/\d+\s*$/ Enum.map(fns, fn a -> - b = Dict.get(specMap, Regex.replace(re, List.to_string(a), "")) - IO.inspect specMap + b = Dict.get(specMap, Regex.replace(re, to_string(a), "")) #IO.inspect b - zipFunSpec.(List.to_string(a),b) + zipFunSpec.(to_string(a),b) end) false -> fns @@ -106,8 +104,15 @@ execute = fn ("l", input) -> require.(input) ("a", input) -> {exists, one, multi} = IEx.Autocomplete.expand(Enum.reverse(to_char_list(input))) - {exists, one, pairWithSpec.(input, multi)} + {exists, one, pairWithSpec.(input, multi, ~r/\.\w*$/)} ("s", input) -> {"True", "" , Map.to_list getSpec.(String.to_atom("Elixir." <> input))} + ("ea", input) -> + ninput = ":"<>String.replace(input, ":", ".") + {exists, one, multi} = IEx.Autocomplete.expand(Enum.reverse(to_char_list(ninput))) + none = String.replace(List.to_string(one), ".", ":") + nmulti = Enum.map multi, &String.replace(to_string(&1), ".", ":") + #nmulti = Enum.map(multi, fn a -> String.replace(to_string(a),".",":") end) + {exists, none, pairWithSpec.(ninput , nmulti, ~r/\.\w*$/)} end loop = fn(y) -> diff --git a/lib/alchemide/doendmatcher.coffee b/lib/alchemide/doendmatcher.coffee new file mode 100644 index 0000000..cb6408c --- /dev/null +++ b/lib/alchemide/doendmatcher.coffee @@ -0,0 +1,39 @@ +DO = /(?!do:)\bdo\b/ +END = /\bend\b/ +FN = /\bfn\b/ +DOEND = /(?!do:)(\bdo\b|\bend\b|\bfn\b)/g +{Range, Point} = require("atom") +decorations = [] + + +highlightRange = (editor, r) -> + marker = editor.markBufferRange(r) + decorations.push editor.decorateMarker(marker, {type: 'highlight', class: 'selection'}) + +module.exports.handleMatch = (editor, e) -> + if not atom.config.get("autocomplete-elixir.matchDoEnd") then return + decorations.map (a) -> a.destroy() + + lastLineNo = editor.buffer.getLines().length - 1 + [x, y] = e.cursor.getBufferPosition().toArray() + fromBeginning = new Range([0,0], [x, y-1]) + toEnd = new Range([x, y+1], [lastLineNo, 0]) + + word = editor.getWordUnderCursor() + counter = 0; + if DO.test(word) + highlightRange(editor, e.cursor.getCurrentWordBufferRange()) + editor.scanInBufferRange DOEND, toEnd, ({range: r, matchText: m, stop }) -> + if DO.test(m) or FN.test(m) then counter++ + if END.test(m) and counter then counter-- + else if !counter + highlightRange(editor, r) + stop() + if END.test(word) + highlightRange(editor, e.cursor.getCurrentWordBufferRange()) + editor.backwardsScanInBufferRange DOEND, fromBeginning, ({range: r, matchText: m, stop }) -> + if END.test(m) then counter++ + if (DO.test(m) || FN.test(m)) && counter then counter-- + else if !counter + highlightRange(editor, r) + stop() diff --git a/lib/alchemide/extend.js b/lib/alchemide/extend.js new file mode 100644 index 0000000..2525e6e --- /dev/null +++ b/lib/alchemide/extend.js @@ -0,0 +1,52 @@ +module.exports = function extend() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0], + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } else if ((typeof target !== 'object' && typeof target !== 'function') || target == null) { + target = {}; + } + + for (; i < length; ++i) { + options = arguments[i]; + // Only deal with non-null/undefined values + if (options != null) { + // Extend the base object + for (name in options) { + src = target[name]; + copy = options[name]; + + // Prevent never-ending loop + if (target !== copy) { + // Recurse if we're merging plain objects or arrays + if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && isArray(src) ? src : []; + } else { + clone = src && isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[name] = extend(deep, clone, copy); + + // Don't bring in undefined values + } else if (typeof copy !== 'undefined') { + target[name] = copy; + } + } + } + } + } + + // Return the modified object + return target; +}; diff --git a/lib/alchemide/jumptodef/import-processor.coffee b/lib/alchemide/jumptodef/import-processor.coffee new file mode 100644 index 0000000..365e03d --- /dev/null +++ b/lib/alchemide/jumptodef/import-processor.coffee @@ -0,0 +1,6 @@ +regex = /(alias|import)\s+([\w.]+),? *(\w+)? *:? *(.+)?/ + +module.exports.getImportsAndAliases = (editor) -> + range = new Range([0,0], editor.cursors[0].getBufferPosition().toArray()) + editor.backwardsScanInBufferRange regex, range, (m) -> + diff --git a/lib/alchemide/jumptodef/import-processor.coffee~ b/lib/alchemide/jumptodef/import-processor.coffee~ new file mode 100644 index 0000000..4b9b231 --- /dev/null +++ b/lib/alchemide/jumptodef/import-processor.coffee~ @@ -0,0 +1,6 @@ +regex = /(alias|import)\s+([\w.]+),? *(\w+)? *:? *(.+)?/g + +module.exports.getImportsAndAliases = (editor) -> + range = new Range([0,0], editor.cursors[0].getBufferPosition().toArray()) + editor.backwardsScanInBufferRange regex, range, (m) -> + diff --git a/lib/alchemide/jumptodef/jumptodef.coffee b/lib/alchemide/jumptodef/jumptodef.coffee new file mode 100644 index 0000000..9550f20 --- /dev/null +++ b/lib/alchemide/jumptodef/jumptodef.coffee @@ -0,0 +1,46 @@ +symbolDict = {}; +filesDict = {} +$$$ = () -> throw new Error("Not implemented yet") + +wordRegex = /[a-zA-Z0-9.:]*/ + +notifyJump = (fun) -> + keyb = atom.keymaps.findKeyBindings({command:"autocomplete-elixir:backward"}) + atom.notifications.addInfo("Jumped to #{fun} definition \n Press #{keyb[0].keystrokes} to return.") +jumpToRange = (r) -> editor.setCursorBufferPosition(r.start) +jumpToRangeInFile = (f, r) -> + atom.workspace.open f, { + initialLine : r.start.row + initialColumn : r.start.column + } +resolveSymbol = $$$ +loadSTDSymbols = $$$ +loadProjectSymbols = $$$ +updateProjectSymbol = $$$ +getImportsAndAliases = $$$ +initRefreshOnFileSave = $$$ + +module.exports.init = (editor) -> + loadSTDSymbols(editor) + loadProjectSymbols(editor) + initRefreshOnFileSave(editor) + +module.exports.jump = (editor) -> + word = editor.getWordUnderCursor({wordRegex}) + aliasDict = getImportsAndAliases(editor) + + console.log(word) + [fun, mod...] = word.split(".").reverse() + mod.reverse() + + [mod, fun] = resolveSymbol(mod, fun, aliasDict) + if !mod.length #we've got alias or local call + console.log("local jump #{fun}") + found = false; + editor.scan new RegExp("def(macro)? #{fun}"), (m) -> + jumpToRange(m.range) + found = true; + notifyJump(fun) + atom.notifications.addInfo("No #{fun} definition found") unless found + else # we've got project or stdlib call + diff --git a/lib/alchemide/jumptodef/project-processor.coffee b/lib/alchemide/jumptodef/project-processor.coffee new file mode 100644 index 0000000..e69de29 diff --git a/test.erl b/lib/alchemide/test.erl similarity index 100% rename from test.erl rename to lib/alchemide/test.erl diff --git a/lib/alchemide/test.ex b/lib/alchemide/test.ex index 48ae82a..75e5805 100644 --- a/lib/alchemide/test.ex +++ b/lib/alchemide/test.ex @@ -1,6 +1,15 @@ defmodule Test do - def function() do - + fn asda -> + for a <- [1,2,3], do: a*2 + end + + def test(a,b) do + Enum. + end +end +defmodule Test do + def test1() do + 1 end end diff --git a/lib/alchemide/typer/.npmignore b/lib/alchemide/typer/.npmignore new file mode 100644 index 0000000..9607671 --- /dev/null +++ b/lib/alchemide/typer/.npmignore @@ -0,0 +1,4 @@ +/_build +/deps +erl_crash.dump +*.ez diff --git a/lib/alchemide/typer/lib/scoper.ex b/lib/alchemide/typer/lib/scoper.ex new file mode 100644 index 0000000..1d8b3fe --- /dev/null +++ b/lib/alchemide/typer/lib/scoper.ex @@ -0,0 +1,44 @@ +defmodule Scoper do + def close_block (code) do + + end + + def compile(string) do + handle(Code.string_to_quoted string ) + end + + def handle {:error, {line, msg, _}} do + + end + def handle {:ok, code} do + + end + + def get_variables({a,_b,c}) when not is_list(c) do + [a] + end + def get_variables(a) when is_tuple(a) do + get_variables(Tuple.to_list(a)) + end + def get_variables([h|t]) do + [get_variables(h) | get_variables(t)] + end + def get_variables(_) do + [] + end + def get_vars(code) do + get_variables(code) + |> List.flatten() + |> Enum.into(HashSet.new()) + |> Set.to_list() + |> Enum.filter(fn a -> Regex.match?(~r/^[^_]/, to_string(a)) end) + |> Enum.map(&to_string/1) + end + + + # def get_variables(other) do + # other + # end + + +end diff --git a/lib/alchemide/typer/lib/specer.ex b/lib/alchemide/typer/lib/specer.ex new file mode 100644 index 0000000..6af2a34 --- /dev/null +++ b/lib/alchemide/typer/lib/specer.ex @@ -0,0 +1,17 @@ +defmodule Specer do + def get_spec(module) do + mapper = fn {{name, _arity}, types} -> + specs = types + |> Enum.map(&(Kernel.Typespec.spec_to_ast(name, &1))) + |> Enum.map(&Macro.to_string/1) + {Atom.to_string(name), specs} + end + reducer = fn {k, v}, acc -> + Dict.put(acc,k,v) + end + + Kernel.Typespec.beam_specs(module) + |> Enum.map(mapper) + |> Enum.reduce(%{}, reducer) + end +end diff --git a/lib/alchemide/typer/test/typer_test.exs b/lib/alchemide/typer/test/typer_test.exs index 178ca94..24e8227 100644 --- a/lib/alchemide/typer/test/typer_test.exs +++ b/lib/alchemide/typer/test/typer_test.exs @@ -1,16 +1,27 @@ defmodule TyperTest do use ExUnit.Case - test "the truth" do - assert 1 + 1 == 2 - end - test "simple expression value" do - assert Typer.get_type(s_to_ast("1")) == :'integer()' - assert Typer.get_type(s_to_ast(":atom")) == :'atom()' - end + test "Scoper" do + res = Scoper.get_vars(quote do + defmodule Specer do + def get_spec(module) do + mapper = fn {{name, _arity}, types} -> + specs = types + |> Enum.map(&(Kernel.Typespec.spec_to_ast(name, &1))) + |> Enum.map(&Macro.to_string/1) + {Atom.to_string(name), specs} + end + reducer = fn {k, v}, acc -> + Dict.put(acc,k,v) + end - def s_to_ast(string) do - {:ok, ast} = Code.string_to_quoted(string) - ast + Kernel.Typespec.beam_specs(module) + |> Enum.map(mapper) + |> Enum.reduce(%{}, reducer) + end + end + end) + assert res == ["v","module","name","types","k","specs","acc","mapper","reducer"] end + end diff --git a/lib/alchemide/wrapper.coffee b/lib/alchemide/wrapper.coffee index 29bbcfd..32b0549 100755 --- a/lib/alchemide/wrapper.coffee +++ b/lib/alchemide/wrapper.coffee @@ -1,32 +1,76 @@ +IS_ELIXIR = true + +extend = require "./extend" autocomplete = "autocompleter/autocomplete.exs" +Process = require("atom").BufferedProcess spawn = require('child_process').spawn path = require 'path' fs = require 'fs' + out = null inp = null +projectPaths = null; +lastError = null; -exports.init = (projectPaths) -> +error = (e) -> atom.notifications.addError("Woops. Something went bananas \n Error: #{e}") #console.log("Err: #{e}") + +exports.init = (pP) -> + projectPaths = pP; p = path.join(__dirname, autocomplete) array = projectPaths + stderr = (e) -> lastError = e #console.log("Err: #{e}") + exit = (e) -> console.error("CLOSED #{e}, Last Error: #{lastError}"); exports.init(projectPaths) + array.push(p) - ac = spawn("elixir", array.reverse()) - out = ac.stdout - inp = ac.stdin - ac.stderr.on("data", (e) -> console.log("Err: #{e}") ) - ac.on("close", (e) -> console.log("CLOSED #{e}"); exports.init(projectPaths)) - ac.stdout.setMaxListeners(1) + name = if IS_ELIXIR then 'autocomplete-elixir' else 'autocomplete-erlang' + setting = atom.config.get("#{name}.elixirPath").replace(/elixir$/,"") + command = path.join ( setting || "") , "elixir" + + #line 1 #line 2 + + erlPath = atom.config.get("#{name}.erlangHome").trim() + env = if erlPath + extend({ + ERL_HOME: erlPath, + ERL_PATH: path.join(erlPath, 'erl') + }, process.env) + else + process.env + options = { + env: env + } + + console.log(setting) + ac = new Process({ + command: command, + options: options, + args: array.reverse(), stderr, exit, stdout: ->}) + unless ac.process then exports.init(pP) + + out = ac.process.stdout + inp = ac.process.stdin + + + exports.getAutocompletion = (prefix, cb) -> + unless inp + exports.init(projectPaths) + return if prefix.trim().length < 1 cb() return - inp.write "a #{prefix}\n" + cmd = if IS_ELIXIR then "a" else "ea" + inp.write "#{cmd} #{prefix}\n" waitTillEnd (chunk) -> [_, one, multi] = chunk.split("<>") cb({one, multi: multi.split(";").filter((a) -> a.trim())}) exports.loadFile = (path, cb = (->)) -> + unless inp + exports.init(projectPaths) + return unless /.ex$/.test(path) cb() return diff --git a/lib/autocomplete-elixir-client.coffee b/lib/autocomplete-elixir-client.coffee index a387ac2..d7d5436 100644 --- a/lib/autocomplete-elixir-client.coffee +++ b/lib/autocomplete-elixir-client.coffee @@ -1,17 +1,24 @@ -$ = require('jquery') autocomplete = require('./alchemide/wrapper') -String.prototype.replaceAll = (s,r) -> @split(s).join(r) +doendmather = require './alchemide/doendmatcher' +jumptodef = require './alchemide/jumptodef/jumptodef.coffee' + +atom.commands.add 'atom-text-editor', + 'autocomplete-elixir:jump-to-definition': (event) -> + editor = @getModel() + if(/.exs?$/.test(editor.getTitle())) + jumptodef.jump(editor) module.exports = class RsenseClient - projectPath: null - serverUrl: null - constructor: -> autocomplete.init(atom.project.getPaths()) atom.workspace.observeTextEditors (editor) -> - editor.onDidSave (e) -> - autocomplete.loadFile(e.path) + # Only if elixir files + if(/.exs?$/.test(editor.getTitle())) + editor.onDidSave (e) -> + autocomplete.loadFile(e.path) + editor.onDidChangeCursorPosition (e) -> + doendmather.handleMatch(editor, e) checkCompletion: (prefix, callback) -> #console.log "Prefix: #{prefix}" diff --git a/lib/autocomplete-elixir-provider.coffee b/lib/autocomplete-elixir-provider.coffee index b046af0..c645363 100644 --- a/lib/autocomplete-elixir-provider.coffee +++ b/lib/autocomplete-elixir-provider.coffee @@ -1,73 +1,89 @@ -RsenseClient = require './autocomplete-elixir-client.coffee' + IS_ELIXIR = true -module.exports = -class RsenseProvider - id: 'autocomplete-elixir-elixirprovider' - selector: '.source.elixir' - rsenseClient: null + lang = if IS_ELIXIR then "elixir" else "erlang" - constructor: -> - @rsenseClient = new RsenseClient() + RsenseClient = require "./autocomplete-#{lang}-client.coffee" - getSuggestions: (request) -> - return new Promise (resolve) => - row = request.bufferPosition.row - col = request.bufferPosition.column + module.exports = + class RsenseProvider + selector: ".source.#{lang}" + rsenseClient: null - prefix = request.editor.getTextInBufferRange([[row ,0],[row, col]]) - [... , prefix] = prefix.split(/[ ()]/) - unless prefix then resolve([]) + constructor: -> + @rsenseClient = new RsenseClient() - completions = @rsenseClient.checkCompletion(prefix, (completions) => - suggestions = @findSuggestions(prefix, completions) - console.log suggestions - return resolve() unless suggestions?.length - return resolve(suggestions) - ) + getSuggestions: (request) -> + return new Promise (resolve) => + row = request.bufferPosition.row + col = request.bufferPosition.column - findSuggestions: (prefix, completions) -> - if completions? - suggestions = [] - for completion in completions when completion.name isnt prefix - one = completion.continuation - [word, spec] = completion.name.trim().split("@") - argTypes = null - ret = null; - if !word || !word[0] then continue - if word[0] == word[0].toUpperCase() then [ret,isModule] = ["Module",true] - label = completion.spec - if spec - specs = spec.replace(/^\w+/,"") - types = specs.substring(1,specs.length-1).split(",") - label = specs - [_, args, ret] = specs.match(/\((.+)\)\s*::\s*(.*)/) - #console.log [args, ret] - argTypes = args.split(",") - count = parseInt(/\d+$/.exec(word)) || 0; - func = /\d+$/.test(word) - if func then word = word.split("/")[0] + "(" - i = 0 - while ++i <= count - if argTypes then word += "${#{i}:#{argTypes[i-1]}}" + (if i != count then "," else "") - else word += "${#{i}:#{i}}" + (if i != count then "," else "") - if func - word += ")" - word += "${#{count+1}:\u0020}" + prefix = request.editor.getTextInBufferRange([[row ,0],[row, col]]) + [... , prefix] = prefix.split(/[ ()]/) + unless prefix then resolve([]) + #TODO check + npref = /.*\./.exec prefix + postfix = "" + if npref + postfix = prefix.replace(npref[0], "") + prefix = npref[0] - [..., last] = prefix.split(".") - suggestion = - snippet: if one then prefix + word else word - prefix: if one then prefix else last - label: if ret then ret else "any" - type: if module then "method" else - if func then "function" else - "variable" - description: spec || ret - #TODO excludeLowerPriority: true + completions = @rsenseClient.checkCompletion(prefix, (completions) => + suggestions = @findSuggestions(prefix, postfix , completions) + return resolve() unless suggestions?.length + return resolve(suggestions) + ) - suggestions.push(suggestion) - return suggestions - return [] + findSuggestions: (prefix, postfix, completions) -> + if completions? + suggestions = [] + for completion in completions when (completion.name isnt prefix+postfix) and (completion.name.indexOf(postfix) == 0) - dispose: -> + one = completion.continuation + [word, spec] = completion.name.trim().split("@") + argTypes = null + ret = null; + if !word || !word[0] then continue + if word[0] == word[0].toUpperCase() then [ret,isModule] = ["Module",true] + label = completion.spec + if spec + specs = spec.replace(/^[\w!?]+/,"") + types = specs.substring(1,specs.length-1).split(",") + label = specs + [_, args, ret] = specs.match(/\(?(.+)\)\s*::\s*(.*)/) + #console.log [args, ret] + argTypes = args.split(",") + count = parseInt(/\d+$/.exec(word)) || 0; + func = /\d+$/.test(word) + if func then word = word.split("/")[0] + "(" + inserted = word; + i = 0 + while ++i <= count + if argTypes then word += "${#{i}:#{argTypes[i-1]}}" + (if i != count then "," else "") + else word += "${#{i}:#{i}}" + (if i != count then "," else "") + inserted += "${#{i}:#{i}}" + (if i != count then "," else "") + + + if func + word += ")${#{count+1}:\u0020}" + inserted += ")${#{count+1}:\u0020}" + [..., last] = (prefix + postfix).split(if IS_ELIXIR then "." else ":") + + type = "variable" + if isModule then type = "method" + if func then type = "function" + + suggestion = + snippet: if one then prefix + postfix + word else word + displayText: (if one then prefix + postfix + word else word).replace(/\(.*\).*}/g, "/#{count}") + replacementPrefix: if one then prefix + postfix else last + rightLabel: if ret then ret else "any" + type: type + description: spec || ret || "Desc" + #inclusionPriority: -1 + #TODO excludeLowerPriority: true + suggestions.push(suggestion) + return suggestions + return [] + + dispose: -> diff --git a/lib/autocomplete-elixir.coffee b/lib/autocomplete-elixir.coffee index 5982479..4562c22 100644 --- a/lib/autocomplete-elixir.coffee +++ b/lib/autocomplete-elixir.coffee @@ -1,18 +1,26 @@ RsenseProvider = require './autocomplete-elixir-provider.coffee' +delorean = require "./delorean/delorean.coffee" module.exports = config: - port: - description: 'The port the rsense server is running on' - type: 'integer' - default: 47367 - minimum: 1024 - maximum: 65535 + matchDoEnd: + type: 'boolean' + default: true + description: "Highlight matching [do|fn]/[end] constructs" + elixirPath: + type: 'string' + default: "" + description: "Absolute path to elixir executable (essential for MacOS)" + erlangHome: + type: 'string' + default: "" + description: "Absolute path to erlang bin directory (essential for MacOS)" rsenseProvider: null activate: (state) -> @rsenseProvider = new RsenseProvider() + delorean.activate(state); provideAutocompletion: -> [@rsenseProvider] diff --git a/lib/delorean/delorean.coffee b/lib/delorean/delorean.coffee new file mode 100644 index 0000000..15cce37 --- /dev/null +++ b/lib/delorean/delorean.coffee @@ -0,0 +1,62 @@ +{CompositeDisposable} = require 'atom' + +module.exports = + subscriptions: null + frontContext: [] + backContext: [] + blockNext : false; + currentContext : null + + activate: -> + @subscriptions = new CompositeDisposable + @subscriptions.add atom.commands.add 'atom-workspace', + 'autocomplete-elixir:backward': => @backward() + 'autocomplete-elixir:forward': => @forward() + self = this + atom.workspace.observeTextEditors (editor) -> + editor.onDidChangeCursorPosition (e) -> + self.contextChanged editor.getPath(), e.newBufferPosition, e.textChanged + + deactivate: -> + @subscriptions.dispose() + + forward: -> + if editor = atom.workspace.getActiveTextEditor() + if context = @frontContext.shift() + @backContext.push @currentContext + @setContext(editor, context) + + backward: -> + if editor = atom.workspace.getActiveTextEditor() + if context = @backContext.pop() + @frontContext.unshift @currentContext + @setContext(editor, context) + + + contextChanged: (file, position, insert) -> + # if cursor moves because of text input + if insert + @currentContext = {file, position} + return; + if @blockNext + @blockNext = false; + return + if @currentContext + @backContext.push @currentContext + # clear redo history + @frontContext = [] + @currentContext = {file, position} + + setContext: (editor, context) -> + @blockNext = true; + @setPosition(editor, context.file, context.position) + @currentContext = context + + setPosition: (editor, file, position) -> + if file == editor.getPath() + editor.setCursorBufferPosition(position); + else + atom.workspace.open file, { + initialLine : position.row + initialColumn : position.column + } diff --git a/package.json b/package.json index c76894e..f66ffa6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "autocomplete-elixir", "main": "./lib/autocomplete-elixir", - "version": "1.2.0", + "version": "1.5.16", "author": { "name": "Krzysztof Wende ", "email": "iraasta@gmail.com", diff --git a/pics/Screen.Shot.2016-02-19.at.17.12.58.png b/pics/Screen.Shot.2016-02-19.at.17.12.58.png new file mode 100644 index 0000000..6729653 Binary files /dev/null and b/pics/Screen.Shot.2016-02-19.at.17.12.58.png differ diff --git a/pics/presentation.png b/pics/presentation.png new file mode 100644 index 0000000..1b15304 Binary files /dev/null and b/pics/presentation.png differ diff --git a/snippets/language-elixir.cson b/snippets/language-elixir.cson index 28dbcf1..0fd1c87 100644 --- a/snippets/language-elixir.cson +++ b/snippets/language-elixir.cson @@ -1,10 +1,28 @@ '.source.elixir': + 'behaviour': + 'prefix': '@behaviour' + 'body': '@behaviour ${1:Behaviour}' + 'spec': + 'prefix': 'spec' + 'body': '@spec ${1:name}(${2:args}) :: ${3:return_type}' + 'doctest': + 'prefix': 'doctest' + 'body': '@doc ~S"""\n ${1:Description}\n\n## Examples\n\n\tiex> ${2:usage}\n\t${3:result}\n\n"""' + 'puts': + 'prefix': 'puts' + 'body': 'IO.puts $1' + 'inspect': + 'prefix': 'inspect' + 'body': 'IO.inspect $1' 'do': 'prefix': 'do' - 'body': 'do\n\t $1 \nend' + 'body': 'do\n\t$1 \nend' 'fn': 'prefix': 'fn' - 'body': 'fn ${1:arg} -> \n\t $2 \nend' + 'body': 'fn ${1:arg} -> $2 end' + 'for': + 'prefix': 'for' + 'body': 'for ${1:x} <- ${2:xs}, do: ${3:x}' 'try-catch': 'prefix' : 'try' 'body': 'try\n\t$1\ncatch\n\t${2:_},${3:_} -> $4 \nend' @@ -26,4 +44,6 @@ 'Agent start' : 'prefix' : 'Agent.start' 'body': 'Agent.start_link(fn -> $1 end)' - + 'Multiline' : + 'prefix' : 'multi' + 'body' : '\"\"\"\n\t$1\n\"\"\"'