Class: Appear::Editor::TmuxIde

Inherits:
Service show all
Defined in:
lib/appear/editor.rb

Overview

TmuxIde is an editor that treasts a collection of Tmux splits holding an Nvim process as an IDE. A “session” is a Tmux window that at least contains an Nvim instance, although new sessions are split like this:


| | | nvim | | | |———–| |$ |$ | | | | |———–|

Instance Method Summary collapse

Methods inherited from BaseService

delegate, require_service, required_services

Constructor Details

#initialize(svcs = {}) ⇒ TmuxIde

Returns a new instance of TmuxIde.



38
39
40
41
# File 'lib/appear/editor.rb', line 38

def initialize(svcs = {})
  super(svcs)
  @tmux_memo = ::Appear::Util::Memoizer.new
end

Instance Method Details

#call(*filenames) ⇒ Object

reveal files in an existing or new IDE session

Parameters:

  • filenames (Array<String>)


173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/appear/editor.rb', line 173

def call(*filenames)
  nvims = []
  nvim_to_session = {}

  filenames.each do |filename|
    filename = File.expand_path(filename)
    nvim, pane = find_or_create_ide(filename)
    # focuses the file in the nvim instance, or start editing it.
    Thread.new { nvim.drop(filename) }
    nvims << nvim unless nvims.include?(nvim)
    nvim_to_session[nvim] = pane.session
  end

  nvims.map do |nvim|
    Thread.new do
      # go ahead and reveal our nvim
      next true if services.revealer.call(nvim.pid)

      session = nvim_to_session[nvim]
      # if we didn't return, we need to create a Tmux client for our
      # session.
      command = services.tmux.attach_session_command(session)
      terminal = services.terminals.get
      term_pane = terminal.new_window(command.to_s)
      terminal.reveal_pane(term_pane)
    end
  end.each(&:join)

  log "#{self.class}: finito."
end

#create_ide(filename) ⇒ Object

Create a new IDE instance editing ‘filename`

Parameters:

  • filename (String)


110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/appear/editor.rb', line 110

def create_ide(filename)
  dir = project_root(filename)

  # find or create session
  tmux_session = services.tmux.sessions.sort_by { |s| s.windows.length }.last
  tmux_session ||= services.tmux.new_session(
    # -c: current directory
    :c => dir
  )

  # find or create window
  window = tmux_session.windows.find do |win|
    win.panes.first.current_path == dir
  end

  window ||= tmux_session.new_window(
    # -c: current directory
    :c => dir,
    # -d: do not focus
    :d => true,
  )

  # remember our pid list
  existing_nvims = services.processes.pgrep(Nvim::NEOVIM)

  # split window across the middle, into a big and little pane
  main = window.panes.first
  main.send_keys([Nvim.edit_command(filename).to_s, "\n"], :l => true)
  left = main.split(:p => 30, :v => true, :c => dir)
  # cut the smaller bottom pane in half
  right = left.split(:p => 50, :h => true, :c => dir)
  # put a vim in the top pane, and select it
  #[left, right].each_with_index do |pane, idx|
    #pane.send_keys(["bottom pane ##{idx}"], :l => true)
  #end

  # Hacky way to wait for nvim to launch! This should take at most 2
  # seconds, otherwise your vim is launching too slowley ;)
  wait_until(2) { (services.processes.pgrep(Nvim::NEOVIM) - existing_nvims).length >= 1 }

  nvim = find_nvim_for_file(filename)
  return nvim, find_tmux_pane(nvim)
end

#find_nvim_for_file(filename) ⇒ ::Appear::Editor::Nvim?

Find the appropriate Nvim session for a given filename. First, we try to find a session actually editing this file. If none exists, we find the session with the deepest CWD that contains the filename.

Parameters:

  • filename (String)

Returns:



61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/appear/editor.rb', line 61

def find_nvim_for_file(filename)
  update_nvims
  cwd_to_nvim = {}

  @nvims.each do |_, nvim|
    return nvim if nvim.find_buffer(filename)
  end

  match = @cwd_by_depth.find { |cwd| path_contains?(cwd, filename) }
  return nil unless match
  @cwd_to_nvim[match]
end

#find_or_create_ide(filename) ⇒ Object

Find or create an IDE, then open this file in it.

Parameters:

  • filename (String)


101
102
103
104
105
# File 'lib/appear/editor.rb', line 101

def find_or_create_ide(filename)
  nvim = find_nvim_for_file(filename)
  return nvim, find_tmux_pane(nvim) unless nvim.nil?
  create_ide(filename)
end

#find_tmux_pane(nvim) ⇒ Appear::Tmux::Pane?

find the tmux pane holding an nvim editor instance.

Parameters:

Returns:



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/appear/editor.rb', line 78

def find_tmux_pane(nvim)
  @tmux_memo.call(nvim) do
    tree = services.processes.process_tree(nvim.pid)
    tmux_server = tree.find { |p| p.name == 'tmux' }
    next nil unless tmux_server

    # the first join should be the tmux pane holding our
    # nvim session.
    proc_and_panes = Util::Join.join(:pid, services.tmux.panes, tree)
    pane_join = proc_and_panes.first
    next nil unless pane_join

    # new method on join: let's you get an underlying
    # object out of the join if it matches a predicate.
    next pane_join.unjoin do |o|
      o.is_a? ::Appear::Tmux::Pane
    end
  end
end

#path_contains?(parent, child) ⇒ Boolean

Check if a child path is contained by a parent path. as dumb as they come TODO: use a real path_contains algorithm.

Parameters:

  • parent (String)
  • child (String)

Returns:

  • (Boolean)


50
51
52
53
# File 'lib/appear/editor.rb', line 50

def path_contains?(parent, child)
  p, c = Pathname.new(parent), Pathname.new(child)
  c.expand_path.to_s.start_with?(p.expand_path.to_s)
end

#project_root(filename) ⇒ String

Guess the project root for a given path by inspecting its parent directories for certain markers like git roots.

Parameters:

  • filename (String)

Returns:

  • (String)

    some path



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/appear/editor.rb', line 209

def project_root(filename)
  # TODO: a real constant? Some internet-provided list?
  # these are files that indicate the root of a project
  markers = %w(.git .hg Gemfile package.json setup.py README README.md)
  p = Pathname.new(filename).expand_path
  p.ascend do |path|
    is_root = markers.any? do |marker|
      path.join(marker).exist?
    end

    return path if is_root
  end

  # no markers were found
  return p.to_s if p.directory?
  return p.dirname.to_s
end

#wait_until(max_duration, sleep = 0.1) ⇒ Object

Raises:

  • (ArgumentError)


154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/appear/editor.rb', line 154

def wait_until(max_duration, sleep = 0.1)
  raise ArgumentError.new("no block given") unless block_given?
  start = Time.new
  limit = start + max_duration
  iters = 0
  while Time.new < limit
    if yield
      log("wait_until(max_duration=#{max_duration}, sleep=#{sleep}) slept #{iters} times, took #{Time.new - start}s")
      return true
    end
    iters = iters + 1
    sleep(sleep)
  end
  false
end