appear 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.doc-coverage +1 -1
- data/CHANGELOG.md +11 -0
- data/README.md +1 -1
- data/bin/appear +6 -0
- data/lib/appear/command.rb +28 -0
- data/lib/appear/config.rb +7 -0
- data/lib/appear/constants.rb +1 -4
- data/lib/appear/editor/nvim.rb +251 -0
- data/lib/appear/editor.rb +262 -0
- data/lib/appear/instance.rb +4 -0
- data/lib/appear/lsof.rb +84 -51
- data/lib/appear/mac_os.rb +12 -2
- data/lib/appear/output.rb +16 -0
- data/lib/appear/processes.rb +4 -6
- data/lib/appear/revealers.rb +65 -62
- data/lib/appear/runner.rb +25 -5
- data/lib/appear/terminal.rb +127 -0
- data/lib/appear/tmux.rb +285 -39
- data/lib/appear/util/command_builder.rb +148 -0
- data/lib/appear/util/join.rb +144 -0
- data/lib/appear/util/memoizer.rb +83 -0
- data/lib/appear/util/value_class.rb +57 -0
- data/lib/appear/util.rb +6 -0
- data/lib/appear.rb +55 -1
- data/scripts/console +9 -1
- data/tools/macOS-helper.js +24 -16
- data/tools/unix-dropper.applescript +167 -0
- metadata +11 -3
- data/lib/appear/join.rb +0 -134
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67b9aebb07fd1277e44209ecc142ce4266b2a614
|
|
4
|
+
data.tar.gz: e234f31f3e772957cf50967eae4ceb633ee1951c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0caad8654c99c2d4b98fe759fb37ad8919443ce332d638af1459494f3614887336ecaf38550ba733bd4a85558c0672e479e2762d16dd1e0619c6b70d1d5ee2d7
|
|
7
|
+
data.tar.gz: 3c98564f230a8a7271920b122d583628155411fa76c0e62b2a49efc9fa677b36b8c49f9ba4dcb30f1a2e6f75f65ed372ec08a7ee4521fa0ec87ef55d003274e0
|
data/.doc-coverage
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
100.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
- new experimental feature: `appear --edit nvim FILES...` to edit files in Nvim inside Tmux
|
|
6
|
+
inside iTerm2. Support is limited.
|
|
7
|
+
- refactors of many core components:
|
|
8
|
+
- Tmux service
|
|
9
|
+
- MacRevealers were split into Terminal services
|
|
10
|
+
- new handy Utils classes in appear/util, including the very handy
|
|
11
|
+
`Appear::Util::CommandBuilder`, and the slightly less useful
|
|
12
|
+
`Appear::Util::Memoizer`.
|
|
13
|
+
|
|
3
14
|
## 1.1.1
|
|
4
15
|
|
|
5
16
|
- passing in a PID was broken.
|
data/README.md
CHANGED
data/bin/appear
CHANGED
data/lib/appear/command.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'appear/constants'
|
|
2
2
|
require 'appear/config'
|
|
3
3
|
require 'appear/instance'
|
|
4
|
+
require 'appear/editor'
|
|
4
5
|
require 'optparse'
|
|
5
6
|
|
|
6
7
|
module Appear
|
|
@@ -34,6 +35,11 @@ module Appear
|
|
|
34
35
|
@config.record_runs = flag
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
o.on('-e', '--edit [EDITOR]', "instead of revealing a PID, edit a file the given editor. Editors: #{::Appear::Editor::ALL.map {|c| c.name}}") do |flag|
|
|
39
|
+
@config.edit_file = true
|
|
40
|
+
@config.editor = flag
|
|
41
|
+
end
|
|
42
|
+
|
|
37
43
|
o.on('--version', 'show version information, then exit') do
|
|
38
44
|
puts "appear #{Appear::VERSION}"
|
|
39
45
|
puts " author: Jake Teton-Landis"
|
|
@@ -60,6 +66,16 @@ module Appear
|
|
|
60
66
|
def execute(all_args)
|
|
61
67
|
argv = option_parser.parse(*all_args)
|
|
62
68
|
|
|
69
|
+
if @config.edit_file
|
|
70
|
+
return execute_edit(argv)
|
|
71
|
+
else
|
|
72
|
+
return execute_pid(argv)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def execute_pid(argv)
|
|
63
79
|
if argv.empty?
|
|
64
80
|
pid = Process.pid
|
|
65
81
|
start_message = "STARTING. pid: #{pid} (current process pid)"
|
|
@@ -83,5 +99,17 @@ module Appear
|
|
|
83
99
|
exit 2
|
|
84
100
|
end
|
|
85
101
|
end
|
|
102
|
+
|
|
103
|
+
def execute_edit(argv)
|
|
104
|
+
raise ArgumentError.new("no file passed") if argv.empty?
|
|
105
|
+
|
|
106
|
+
revealer = Appear::Instance.new(@config)
|
|
107
|
+
# this is a sin, for now
|
|
108
|
+
# TODO: remove instance_variable_get, use a real API
|
|
109
|
+
services = revealer.instance_variable_get('@all_services')
|
|
110
|
+
ide = ::Appear::Editor::TmuxIde.new(services)
|
|
111
|
+
ide.call(*argv)
|
|
112
|
+
end
|
|
113
|
+
|
|
86
114
|
end
|
|
87
115
|
end
|
data/lib/appear/config.rb
CHANGED
|
@@ -15,10 +15,17 @@ module Appear
|
|
|
15
15
|
# @return [Boolean] default false
|
|
16
16
|
attr_accessor :record_runs
|
|
17
17
|
|
|
18
|
+
# @return [Boolean] default false
|
|
19
|
+
attr_accessor :edit_file
|
|
20
|
+
|
|
21
|
+
# @return [String, nil] default nil
|
|
22
|
+
attr_accessor :editor
|
|
23
|
+
|
|
18
24
|
# sets defaults
|
|
19
25
|
def initialize
|
|
20
26
|
self.silent = true
|
|
21
27
|
self.record_runs = false
|
|
28
|
+
self.edit_file = false
|
|
22
29
|
end
|
|
23
30
|
end
|
|
24
31
|
end
|
data/lib/appear/constants.rb
CHANGED
|
@@ -2,14 +2,11 @@ require 'pathname'
|
|
|
2
2
|
|
|
3
3
|
module Appear
|
|
4
4
|
# the version of Appear
|
|
5
|
-
VERSION = '1.
|
|
5
|
+
VERSION = '1.2.0'
|
|
6
6
|
|
|
7
7
|
# root error for our library; all other errors inherit from this one.
|
|
8
8
|
class Error < StandardError; end
|
|
9
9
|
|
|
10
|
-
# we also put common constants in here because it's a good spot.
|
|
11
|
-
# Should we rename this file to 'appear/constants' ?
|
|
12
|
-
|
|
13
10
|
# the root of the Appear project directory
|
|
14
11
|
MODULE_DIR = Pathname.new(__FILE__).realpath.join('../../..')
|
|
15
12
|
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
require 'appear/util/command_builder'
|
|
2
|
+
require 'appear/util/value_class'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module Appear
|
|
7
|
+
module Editor
|
|
8
|
+
# Raised if we have problems interacting with Nvim.
|
|
9
|
+
class NvimError < ::Appear::Error
|
|
10
|
+
attr_reader :from_err
|
|
11
|
+
# @param msg [String]
|
|
12
|
+
# @param from_err [Exception] pass in another error to wrap it in an
|
|
13
|
+
# NvimError.
|
|
14
|
+
def initialize(msg, from_err = nil)
|
|
15
|
+
super(msg)
|
|
16
|
+
@from_err = from_err
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Wraps nvim-remote to implement basic nvim support.
|
|
21
|
+
# @see https://github.com/mhinz/neovim-remote
|
|
22
|
+
#
|
|
23
|
+
# I put this in my zshrc:
|
|
24
|
+
# `export NVIM_LISTEN_ADDRESS="$HOME/.vim/sockets/vim-zsh-$$.sock"`
|
|
25
|
+
# this opens a seperate nvim socket for each new Zsh shell. You'll still
|
|
26
|
+
# get nvims trying to open the same socket if you use ctrl-z and fg and
|
|
27
|
+
# stuff to manage 'em, but overall this solves needing command a bunch of
|
|
28
|
+
# different things.
|
|
29
|
+
class Nvim < Service
|
|
30
|
+
# the `neovim-remote` command name
|
|
31
|
+
NVR = 'nvr'.freeze
|
|
32
|
+
# the `nvim` command name
|
|
33
|
+
NEOVIM = 'nvim'.freeze
|
|
34
|
+
|
|
35
|
+
# the value to use for Vim buffers with no name. This is the UI value
|
|
36
|
+
# that Vim usually shows.
|
|
37
|
+
NO_NAME = "[No Name]".freeze
|
|
38
|
+
|
|
39
|
+
# value class describing a vim pane.
|
|
40
|
+
class Pane < Util::ValueClass
|
|
41
|
+
# @return [Fixnum] vim tab number
|
|
42
|
+
property :tab
|
|
43
|
+
|
|
44
|
+
# @return [Fixnum] vim window number
|
|
45
|
+
property :window
|
|
46
|
+
|
|
47
|
+
# @return [Fixnum] vim buffer number
|
|
48
|
+
property :buffer
|
|
49
|
+
|
|
50
|
+
# @return [Hash<Symbol, String>] data about the buffer
|
|
51
|
+
property :buffer_info
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
delegate :run, :runner
|
|
55
|
+
|
|
56
|
+
# This constant maps logical name to a string of filename-modifiers, for
|
|
57
|
+
# the vim fnamemodify() function. When we collect information about
|
|
58
|
+
# buffers, we get each of these expansions applied to the buffer name.
|
|
59
|
+
#
|
|
60
|
+
# This mapping was copied from the BufExplorer vim plugin.
|
|
61
|
+
BUFFER_FILENAME_EXPANSIONS = {
|
|
62
|
+
:name => '',
|
|
63
|
+
:absolute_path => ':p',
|
|
64
|
+
:dirname => ':p:h',
|
|
65
|
+
:relative_path => ':~:.',
|
|
66
|
+
:relative_dirname => ':~:.:h',
|
|
67
|
+
:shortname => ':t'
|
|
68
|
+
}.freeze
|
|
69
|
+
# order in which we pass these to vim.
|
|
70
|
+
BUFFER_FILENAME_ORDER = BUFFER_FILENAME_EXPANSIONS.keys.freeze
|
|
71
|
+
|
|
72
|
+
# @return [#to_s] path to the unix socket used to talk to Nvim
|
|
73
|
+
attr_reader :socket
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# List all the sockets found in ~/.vim/sockets.
|
|
77
|
+
# I put this in my zshrc to make this work:
|
|
78
|
+
# `export NVIM_LISTEN_ADDRESS="$HOME/.vim/sockets/vim-zsh-$$.sock"`
|
|
79
|
+
# @return [Array<Pathname>]
|
|
80
|
+
def self.sockets
|
|
81
|
+
Dir.glob(File.expand_path('~/.vim/sockets/*.sock'))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Spawn a new NVIM instance, then connect to its socket.
|
|
85
|
+
def self.edit_command(filename)
|
|
86
|
+
::Appear::Util::CommandBuilder.new(NEOVIM).args(filename)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param socket [#to_s] UNIX socket to use to talk to Nvim.
|
|
90
|
+
def initialize(socket, svc = {})
|
|
91
|
+
super(svc)
|
|
92
|
+
@socket = socket
|
|
93
|
+
@expr_memo = ::Appear::Util::Memoizer.new
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# evaluate a Vimscript expression
|
|
97
|
+
#
|
|
98
|
+
# @param vimstring [String] the expression, eg "fnamemodify('~', ':p')"
|
|
99
|
+
# @return [Object] expression result, parsed as YAML.
|
|
100
|
+
def expr(vimstring)
|
|
101
|
+
@expr_memo.call(vimstring) do
|
|
102
|
+
parse_output_as_yaml(run(command.flag('remote-expr', vimstring).to_a))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Perform a Vim command
|
|
107
|
+
#
|
|
108
|
+
# @param vimstring [String] the command, eg ":buffers"
|
|
109
|
+
def cmd(vimstring)
|
|
110
|
+
run(command.flag('c', vimstring).to_a)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# PID of the remote vim session
|
|
114
|
+
#
|
|
115
|
+
# @return [Fixnum]
|
|
116
|
+
def pid
|
|
117
|
+
expr('getpid()')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Working directory of the remote vim session
|
|
121
|
+
#
|
|
122
|
+
# @return [Pathname]
|
|
123
|
+
def cwd
|
|
124
|
+
expr('getcwd()')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get all the Vim panes in all tabs.
|
|
128
|
+
#
|
|
129
|
+
# @return [Array<Pane>] data
|
|
130
|
+
def panes
|
|
131
|
+
@expr_memo.call(nil, 'panes') do
|
|
132
|
+
all_buffers = get_buffers
|
|
133
|
+
all_panes = []
|
|
134
|
+
get_windows.each_with_index do |wins, tab_idx|
|
|
135
|
+
wins.each_with_index do |buffer, win_idx|
|
|
136
|
+
all_panes << Pane.new(
|
|
137
|
+
# in Vim, tabs and windows start indexing at 1
|
|
138
|
+
:tab => tab_idx + 1,
|
|
139
|
+
:window => win_idx + 1,
|
|
140
|
+
:buffer => buffer,
|
|
141
|
+
:buffer_info => all_buffers[buffer - 1]
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
all_panes
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Find the buffer for a given filename.
|
|
150
|
+
#
|
|
151
|
+
# @param filename [String]
|
|
152
|
+
# @return [Hash, nil] buffer info, nil if not found
|
|
153
|
+
def find_buffer(filename)
|
|
154
|
+
p = File.expand_path(filename)
|
|
155
|
+
get_buffers.find do |buffer|
|
|
156
|
+
buffer[:absolute_path] == p
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Find the pane for a given filename.
|
|
161
|
+
#
|
|
162
|
+
# @param filename [String]
|
|
163
|
+
# @return [Pane, nil] nil if not found
|
|
164
|
+
def find_pane(filename)
|
|
165
|
+
p = Pathname.new(filename).expand_path.to_s
|
|
166
|
+
panes.find do |pane|
|
|
167
|
+
buff = pane.buffer_info
|
|
168
|
+
buff[:absolute_path] == p
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Open a file in vim, or focus a window editing this file, if it's
|
|
173
|
+
# already open. Vim has a command a lot like this, but I can't get it to
|
|
174
|
+
# work in either of [vim,nvim] the way the docs say it should. That's why
|
|
175
|
+
# it's re-implemented here.
|
|
176
|
+
#
|
|
177
|
+
# @param filename [String]
|
|
178
|
+
def drop(filename)
|
|
179
|
+
# not using find_buffer because we care about panes; we'll have to open
|
|
180
|
+
# a new one if one doesn't exist anyways.
|
|
181
|
+
pane = find_pane(filename)
|
|
182
|
+
|
|
183
|
+
if pane
|
|
184
|
+
reveal_pane(pane)
|
|
185
|
+
return pane
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# TODO: consider options other than vsplit
|
|
189
|
+
# TODO: such as opening in the main window with :edit if the main
|
|
190
|
+
# window is an empty [NoName] buffer, from a new vim maybe?
|
|
191
|
+
cmd("vertical split #{filename.shellescape}")
|
|
192
|
+
find_pane(filename)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Reveal a pane in Nvim
|
|
196
|
+
#
|
|
197
|
+
# @param pane [Pane]
|
|
198
|
+
def reveal_pane(pane)
|
|
199
|
+
# go to tab
|
|
200
|
+
cmd("tabnext #{pane.tab}")
|
|
201
|
+
# go to window
|
|
202
|
+
cmd("#{pane.window} . wincmd w")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Description of this Neovim socket and session.
|
|
206
|
+
# @return [String]
|
|
207
|
+
def to_s
|
|
208
|
+
"#<#{self.class.name}:0x#{'%x' % (object_id << 1)} in #{cwd.inspect}>"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def command
|
|
214
|
+
::Appear::Util::CommandBuilder.new(NVR).flags(:servername => @socket.to_s)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Vimscript return values look vaguely like YAML, so parse them with
|
|
218
|
+
# YAML.
|
|
219
|
+
# @param output [String]
|
|
220
|
+
def parse_output_as_yaml(output)
|
|
221
|
+
begin
|
|
222
|
+
return YAML.load(output)
|
|
223
|
+
rescue => err
|
|
224
|
+
raise NvimError.new(output, err)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def get_windows
|
|
229
|
+
expr(%Q[map( range(1, tabpagenr('$')), "tabpagebuflist(v:val)" )])
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def get_buffers
|
|
233
|
+
cmd = BUFFER_FILENAME_ORDER.map do |type|
|
|
234
|
+
"fnamemodify(bufname(v:val), '#{BUFFER_FILENAME_EXPANSIONS[type]}')"
|
|
235
|
+
end.join(', ')
|
|
236
|
+
|
|
237
|
+
as_a = expr(%Q(map( range(1, bufnr('$')), "[v:val, #{cmd} ]" )))
|
|
238
|
+
as_a.map do |row|
|
|
239
|
+
row = row.dup
|
|
240
|
+
buf = {:buffer => row.shift}
|
|
241
|
+
row.each_with_index do |it, i|
|
|
242
|
+
buf[BUFFER_FILENAME_ORDER[i]] = it
|
|
243
|
+
end
|
|
244
|
+
buf[:name] = NO_NAME if buf[:name].empty?
|
|
245
|
+
buf
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
require 'appear/util/command_builder'
|
|
2
|
+
require 'appear/util/join'
|
|
3
|
+
|
|
4
|
+
module Appear
|
|
5
|
+
# Appear::Editor is a sub-library of Appear, for appearing files, instead of
|
|
6
|
+
# processes. Appear::Editor's job is to open a given file in an existing
|
|
7
|
+
# editor session, or start a new session editing that file.
|
|
8
|
+
#
|
|
9
|
+
# Appear::Editor should feature drivers for many editors in the future, but
|
|
10
|
+
# for now only drives Neovim via [neovim-remote][nvr]. We should also support
|
|
11
|
+
# treating `tmux` as an editor, and launching tmux sessions and creating
|
|
12
|
+
# windows and splits as such.
|
|
13
|
+
#
|
|
14
|
+
# @example define my editor
|
|
15
|
+
#
|
|
16
|
+
# [nvr]: https://github.com/mhinz/neovim-remote
|
|
17
|
+
module Editor
|
|
18
|
+
# TmuxIde is an editor that treasts a collection of Tmux splits holding an
|
|
19
|
+
# Nvim process as an IDE. A "session" is a Tmux window that at least
|
|
20
|
+
# contains an Nvim instance, although new sessions are split like this:
|
|
21
|
+
# -------------
|
|
22
|
+
# | |
|
|
23
|
+
# | nvim |
|
|
24
|
+
# | |
|
|
25
|
+
# |-----------|
|
|
26
|
+
# |$ |$ |
|
|
27
|
+
# | | |
|
|
28
|
+
# |-----------|
|
|
29
|
+
class TmuxIde < Service
|
|
30
|
+
|
|
31
|
+
require_service :revealer
|
|
32
|
+
require_service :processes
|
|
33
|
+
require_service :tmux
|
|
34
|
+
require_service :runner # needed for sub-services
|
|
35
|
+
require_service :lsof # needed for sub-services
|
|
36
|
+
require_service :terminals
|
|
37
|
+
|
|
38
|
+
def initialize(svcs = {})
|
|
39
|
+
super(svcs)
|
|
40
|
+
@tmux_memo = ::Appear::Util::Memoizer.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if a child path is contained by a parent path.
|
|
44
|
+
# @param parent [String]
|
|
45
|
+
# @param child [String]
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
#
|
|
48
|
+
# as dumb as they come
|
|
49
|
+
# TODO: use a real path_contains algorithm.
|
|
50
|
+
def path_contains?(parent, child)
|
|
51
|
+
p, c = Pathname.new(parent), Pathname.new(child)
|
|
52
|
+
c.expand_path.to_s.start_with?(p.expand_path.to_s)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Find the appropriate Nvim session for a given filename. First, we try
|
|
56
|
+
# to find a session actually editing this file. If none exists, we find
|
|
57
|
+
# the session with the deepest CWD that contains the filename.
|
|
58
|
+
#
|
|
59
|
+
# @param filename [String]
|
|
60
|
+
# @return [::Appear::Editor::Nvim, nil]
|
|
61
|
+
def find_nvim_for_file(filename)
|
|
62
|
+
update_nvims
|
|
63
|
+
cwd_to_nvim = {}
|
|
64
|
+
|
|
65
|
+
@nvims.each do |_, nvim|
|
|
66
|
+
return nvim if nvim.find_buffer(filename)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
match = @cwd_by_depth.find { |cwd| path_contains?(cwd, filename) }
|
|
70
|
+
return nil unless match
|
|
71
|
+
@cwd_to_nvim[match]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# find the tmux pane holding an nvim editor instance.
|
|
75
|
+
#
|
|
76
|
+
# @param nvim [Appear::Editor::Nvim]
|
|
77
|
+
# @return [Appear::Tmux::Pane, nil] the pane, or nil if not found
|
|
78
|
+
def find_tmux_pane(nvim)
|
|
79
|
+
@tmux_memo.call(nvim) do
|
|
80
|
+
tree = services.processes.process_tree(nvim.pid)
|
|
81
|
+
tmux_server = tree.find { |p| p.name == 'tmux' }
|
|
82
|
+
next nil unless tmux_server
|
|
83
|
+
|
|
84
|
+
# the first join should be the tmux pane holding our
|
|
85
|
+
# nvim session.
|
|
86
|
+
proc_and_panes = Util::Join.join(:pid, services.tmux.panes, tree)
|
|
87
|
+
pane_join = proc_and_panes.first
|
|
88
|
+
next nil unless pane_join
|
|
89
|
+
|
|
90
|
+
# new method on join: let's you get an underlying
|
|
91
|
+
# object out of the join if it matches a predicate.
|
|
92
|
+
next pane_join.unjoin do |o|
|
|
93
|
+
o.is_a? ::Appear::Tmux::Pane
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Find or create an IDE, then open this file in it.
|
|
99
|
+
#
|
|
100
|
+
# @param filename [String]
|
|
101
|
+
def find_or_create_ide(filename)
|
|
102
|
+
nvim = find_nvim_for_file(filename)
|
|
103
|
+
return nvim, find_tmux_pane(nvim) unless nvim.nil?
|
|
104
|
+
create_ide(filename)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Create a new IDE instance editing `filename`
|
|
108
|
+
#
|
|
109
|
+
# @param filename [String]
|
|
110
|
+
def create_ide(filename)
|
|
111
|
+
dir = project_root(filename)
|
|
112
|
+
|
|
113
|
+
# find or create session
|
|
114
|
+
tmux_session = services.tmux.sessions.sort_by { |s| s.windows.length }.last
|
|
115
|
+
tmux_session ||= services.tmux.new_session(
|
|
116
|
+
# -c: current directory
|
|
117
|
+
:c => dir
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# find or create window
|
|
121
|
+
window = tmux_session.windows.find do |win|
|
|
122
|
+
win.panes.first.current_path == dir
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
window ||= tmux_session.new_window(
|
|
126
|
+
# -c: current directory
|
|
127
|
+
:c => dir,
|
|
128
|
+
# -d: do not focus
|
|
129
|
+
:d => true,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# remember our pid list
|
|
133
|
+
existing_nvims = services.processes.pgrep(Nvim::NEOVIM)
|
|
134
|
+
|
|
135
|
+
# split window across the middle, into a big and little pane
|
|
136
|
+
main = window.panes.first
|
|
137
|
+
main.send_keys([Nvim.edit_command(filename).to_s, "\n"], :l => true)
|
|
138
|
+
left = main.split(:p => 30, :v => true, :c => dir)
|
|
139
|
+
# cut the smaller bottom pane in half
|
|
140
|
+
right = left.split(:p => 50, :h => true, :c => dir)
|
|
141
|
+
# put a vim in the top pane, and select it
|
|
142
|
+
#[left, right].each_with_index do |pane, idx|
|
|
143
|
+
#pane.send_keys(["bottom pane ##{idx}"], :l => true)
|
|
144
|
+
#end
|
|
145
|
+
|
|
146
|
+
# Hacky way to wait for nvim to launch! This should take at most 2
|
|
147
|
+
# seconds, otherwise your vim is launching too slowley ;)
|
|
148
|
+
wait_until(2) { (services.processes.pgrep(Nvim::NEOVIM) - existing_nvims).length >= 1 }
|
|
149
|
+
|
|
150
|
+
nvim = find_nvim_for_file(filename)
|
|
151
|
+
return nvim, find_tmux_pane(nvim)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def wait_until(max_duration, sleep = 0.1)
|
|
155
|
+
raise ArgumentError.new("no block given") unless block_given?
|
|
156
|
+
start = Time.new
|
|
157
|
+
limit = start + max_duration
|
|
158
|
+
iters = 0
|
|
159
|
+
while Time.new < limit
|
|
160
|
+
if yield
|
|
161
|
+
log("wait_until(max_duration=#{max_duration}, sleep=#{sleep}) slept #{iters} times, took #{Time.new - start}s")
|
|
162
|
+
return true
|
|
163
|
+
end
|
|
164
|
+
iters = iters + 1
|
|
165
|
+
sleep(sleep)
|
|
166
|
+
end
|
|
167
|
+
false
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# reveal files in an existing or new IDE session
|
|
171
|
+
#
|
|
172
|
+
# @param filenames [Array<String>]
|
|
173
|
+
def call(*filenames)
|
|
174
|
+
nvims = []
|
|
175
|
+
nvim_to_session = {}
|
|
176
|
+
|
|
177
|
+
filenames.each do |filename|
|
|
178
|
+
filename = File.expand_path(filename)
|
|
179
|
+
nvim, pane = find_or_create_ide(filename)
|
|
180
|
+
# focuses the file in the nvim instance, or start editing it.
|
|
181
|
+
Thread.new { nvim.drop(filename) }
|
|
182
|
+
nvims << nvim unless nvims.include?(nvim)
|
|
183
|
+
nvim_to_session[nvim] = pane.session
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
nvims.map do |nvim|
|
|
187
|
+
Thread.new do
|
|
188
|
+
# go ahead and reveal our nvim
|
|
189
|
+
next true if services.revealer.call(nvim.pid)
|
|
190
|
+
|
|
191
|
+
session = nvim_to_session[nvim]
|
|
192
|
+
# if we didn't return, we need to create a Tmux client for our
|
|
193
|
+
# session.
|
|
194
|
+
command = services.tmux.attach_session_command(session)
|
|
195
|
+
terminal = services.terminals.get
|
|
196
|
+
term_pane = terminal.new_window(command.to_s)
|
|
197
|
+
terminal.reveal_pane(term_pane)
|
|
198
|
+
end
|
|
199
|
+
end.each(&:join)
|
|
200
|
+
|
|
201
|
+
log "#{self.class}: finito."
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Guess the project root for a given path by inspecting its parent
|
|
205
|
+
# directories for certain markers like git roots.
|
|
206
|
+
#
|
|
207
|
+
# @param filename [String]
|
|
208
|
+
# @return [String] some path
|
|
209
|
+
def project_root(filename)
|
|
210
|
+
# TODO: a real constant? Some internet-provided list?
|
|
211
|
+
# these are files that indicate the root of a project
|
|
212
|
+
markers = %w(.git .hg Gemfile package.json setup.py README README.md)
|
|
213
|
+
p = Pathname.new(filename).expand_path
|
|
214
|
+
p.ascend do |path|
|
|
215
|
+
is_root = markers.any? do |marker|
|
|
216
|
+
path.join(marker).exist?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
return path if is_root
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# no markers were found
|
|
223
|
+
return p.to_s if p.directory?
|
|
224
|
+
return p.dirname.to_s
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
# creates a cached mapping of Nvim-related information for maximum lookup
|
|
230
|
+
# speed.
|
|
231
|
+
def update_nvims
|
|
232
|
+
@nvims ||= {}
|
|
233
|
+
@nvim_to_cwd ||= {}
|
|
234
|
+
@cwd_to_nvim ||= {}
|
|
235
|
+
new_nvims = false
|
|
236
|
+
|
|
237
|
+
Nvim.sockets.each do |sock|
|
|
238
|
+
next if @nvims[sock]
|
|
239
|
+
|
|
240
|
+
new_nvims = true
|
|
241
|
+
nvim = Nvim.new(sock, services)
|
|
242
|
+
@nvims[sock] = nvim
|
|
243
|
+
cwd = nvim.cwd
|
|
244
|
+
@nvim_to_cwd[nvim] = cwd
|
|
245
|
+
@cwd_to_nvim[cwd] = nvim
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
if new_nvims
|
|
249
|
+
@cwd_by_depth = @cwd_to_nvim.keys.sort_by { |d| Pathname.new(d).each_filename.to_a.length }
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# List of all editors. In the future, we should provide more editor support
|
|
256
|
+
# and extend this list, and then allow the user to choose the editor to
|
|
257
|
+
# Appear in.
|
|
258
|
+
ALL = [TmuxIde]
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
require 'appear/editor/nvim'
|
data/lib/appear/instance.rb
CHANGED
|
@@ -32,6 +32,10 @@ module Appear
|
|
|
32
32
|
@all_services[:lsof] = Appear::Lsof.new(@all_services)
|
|
33
33
|
@all_services[:mac_os] = Appear::MacOs.new(@all_services)
|
|
34
34
|
@all_services[:tmux] = Appear::Tmux.new(@all_services)
|
|
35
|
+
@all_services[:terminals] = Appear::Terminals.new([
|
|
36
|
+
Appear::Terminal::Iterm2.new(@all_services),
|
|
37
|
+
Appear::Terminal::TerminalApp.new(@all_services),
|
|
38
|
+
])
|
|
35
39
|
|
|
36
40
|
# make sure we can use our processes service, and log stuff.
|
|
37
41
|
super(@all_services)
|