require 'fileutils'
require 'rbconfig'
require 'pstree'
module Utils
# An editor interface for interacting with Vim server instances.
#
# This class provides functionality for managing Vim editor sessions through
# server connections, enabling features like remote file editing, window
# management, and server state monitoring. It handles communication with
# running Vim instances and supports various configuration options for
# customizing the editing experience.
#
# @example
# editor = Utils::Editor.new
# editor.edit('file.rb')
# editor.activate
# editor.stop
class Editor
# The initialize method sets up a new editor instance with default
# configuration.
#
# This method configures the editor by initializing default values for wait
# flag, pause duration, and server name. It also loads the configuration
# file and assigns the edit configuration section to the instance.
#
# @yield |editor| optional block to be executed after initialization with
# self as argument.
#
# @return [ Utils::Editor ] a new editor instance configured with default settings
def initialize
self.wait = false
self.pause_duration = 1
self.servername = derive_server_name
config = Utils::ConfigFile.new
config.configure_from_paths
self.config = config.edit
yield self if block_given?
end
# The derive_server_name method constructs a server name based on
# environment configuration.
#
# This method determines an appropriate server name by checking for a
# VIM_SERVER environment variable, falling back to the current working
# directory if not set. On Windows-like systems, it prefixes the name with
# "G_" to ensure uniqueness. The resulting name is converted to uppercase
# for consistent formatting.
#
# @return [ String ] the constructed server name based on environment and
# system configuration
private def derive_server_name
name = ENV['VIM_SERVER'] || Dir.pwd
RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ and name = "G_#{name}"
name.upcase
end
# The pause_duration method provides access to the duration value used for
# pausing operations.
#
# This method returns the current value of the pause duration attribute,
# which controls how long certain operations should wait or pause between
# actions.
#
# @return [ Integer, Float ] the current pause duration value in seconds
attr_accessor :pause_duration
# The wait method gets the wait status.
#
# @return [ TrueClass, FalseClass, nil ] the wait status value
attr_accessor :wait
alias wait? wait
# The servername method provides access to the server name attribute.
#
# This method returns the value of the server name instance variable,
# which represents the name of the server being used.
#
# @return [ String ] the server name value
attr_accessor :servername
# The mkdir method provides access to the directory creation flag.
#
# This method returns the current value of the mkdir flag, which determines
# whether directory creation should be attempted when processing files.
#
# @return [ TrueClass, FalseClass ] the current state of the mkdir flag
attr_accessor :mkdir
# The config method provides access to the configuration object.
#
# This method returns the configuration instance variable that holds the
# settings and options for the object's operation.
#
# @return [ Utils::ConfigFile ] the configuration object associated with this instance
attr_accessor :config
# The vim method constructs and returns the Vim command configuration.
#
# This method assembles the Vim command by combining the configured Vim
# path with any default arguments specified in the configuration.
#
# @return [ Array<String> ] an array containing the Vim executable path and
# its default arguments for command execution
def vim
([ config.vim_path ] + Array(config.vim_default_args))
end
# The cmd method constructs a command from parts and executes it.
#
# This method takes multiple arguments, processes them to build a command
# array, and then executes the command using the system call.
#
# @param parts [ Array ] the parts to be included in the command
#
# @return [ Boolean ] true if the command was successful, false otherwise
def cmd(*parts)
command = parts.compact.inject([]) do |a, p|
case
when p == nil, p == []
a
when p.respond_to?(:to_ary)
a.concat p.to_ary
else
a << p.to_s
end
end
$DEBUG and warn command * ' '
system(*command.map(&:to_s))
end
# The fullscreen= method sets the fullscreen state for the remote editor
# session.
#
# This method configures the fullscreen mode of the remote editor by
# sending appropriate commands through the edit_remote_send mechanism. It
# ensures the editor session is started and paused briefly before applying
# the fullscreen
# setting, then activates the session to apply the changes.
#
# @param enabled [ TrueClass, FalseClass ] determines whether to enable or
# disable fullscreen mode
def fullscreen=(enabled)
start
sleep pause_duration
if enabled
edit_remote_send '<ESC>:set fullscreen<CR>'
else
edit_remote_send '<ESC>:set nofullscreen<CR>'
end
activate
end
# The file_linenumber? method checks if a filename matches the file and
# line number pattern.
#
# This method determines whether the provided filename string conforms to
# the regular expression pattern used for identifying file paths
# accompanied by line numbers.
#
# @param filename [ String ] the filename string to be checked
#
# @return [ MatchData, nil ] a match data object if the filename matches the pattern,
# or nil if it does not match
def file_linenumber?(filename)
filename.match(Utils::Xt::SourceLocationExtension::FILE_LINENUMBER_REGEXP)
end
# The expand_globs method processes an array of filename patterns by
# expanding glob expressions and returning a sorted array of unique
# filenames.
#
# @param filenames [ Array<String> ] an array of filename patterns that may
# include glob expressions
#
# @return [ Array<String> ] a sorted array of unique filenames with glob
# patterns expanded, or the original array if no glob patterns are present
def expand_globs(filenames)
filenames.map { |f| Dir[f] }.flatten.uniq.sort.full? || filenames
end
# The edit method processes filenames to determine their source location
# and delegates to appropriate editing methods.
#
# If a single filename is provided and it has a source location, the method
# checks whether the location includes filename and linenumber attributes.
# If so, it calls edit_source_location with the source location; otherwise,
# it calls edit_file_linenumber with the source location components.
# If multiple filenames are provided and all have source locations, the
# method expands any glob patterns in the filenames, then calls edit_file
# with the expanded list of filenames.
# Finally, it ensures the editor is activated after processing.
#
# @param filenames [ Array<String, Integer> ] an array of filenames that
# may contain source location information
def edit(*filenames)
source_location = nil
if filenames.size == 1 and
source_location = filenames.first.source_location
then
if source_location.respond_to?(:filename) and source_location.respond_to?(:linenumber)
edit_source_location(source_location)
else
edit_file_linenumber(*source_location)
end
elsif source_locations = filenames.map(&:source_location).compact.full?
filenames = expand_globs(source_locations.map(&:first))
edit_file(*filenames)
end.tap do
activate
end
end
# The make_dirs method creates directory structures for the provided
# filenames.
#
# This method checks if directory creation is enabled and, if so, ensures
# that the parent directories for each filename exist by creating them
# recursively.
#
# @param filenames [ Array<String> ] an array of filenames for which to
# create directory structures
private def make_dirs(*filenames)
if mkdir
for filename in filenames
FileUtils.mkdir_p File.dirname(filename)
end
end
end
# The edit_file method processes a list of filenames by ensuring their
# directories exist and then delegates to a remote file editing function.
#
# @param filenames [ Array<String> ] an array of filename strings to be processed
def edit_file(*filenames)
make_dirs(*filenames)
edit_remote_file(*filenames)
end
# The edit_file_linenumber method opens a file at a specific line number
# and optionally selects a range of lines in an editor.
#
# @param filename [ String ] the path to the file to be opened
# @param linenumber [ Integer ] the line number where the file should be
# opened
# @param rangeend [ Integer, nil ] the ending line number for selection, or
# nil if no range is specified
def edit_file_linenumber(filename, linenumber, rangeend = nil)
make_dirs filename
if rangeend
Thread.new do
while !started?
sleep 1
end
edit_remote_send("<ESC>:normal #{linenumber}GV#{rangeend}G<CR>")
end
end
if wait?
activate
edit_remote_wait("+#{linenumber}", filename)
else
edit_remote("+#{linenumber}", filename)
end
end
# The edit_source_location method processes a source location object to
# open the corresponding file at the specified line number.
#
# This method takes a source location object and uses its filename, line
# number, and optional range end to invoke the edit_file_linenumber method
# for opening the file in an editor.
#
# @param source_location [ Array<String, Integer> ] the source location
# containing filename and line number information
def edit_source_location(source_location)
edit_file_linenumber(
source_location.filename,
source_location.linenumber,
source_location.rangeend
)
end
# The rename_window method renames the current tmux window to match the
# base name of the current script.
#
# This method checks if the application is running within a tmux session
# and, if so, renames the current window to reflect the base name of the
# script being executed. It only performs the renaming operation if a tmux
# session is detected and the window has not already been started.
private def rename_window
return if started?
ENV['TMUX'] and system "tmux rename-window #{File.basename($0)}"
end
# The start method initializes the Vim server connection if it is not
# already running.
#
# This method first attempts to rename the terminal window to reflect the
# server name, then checks if the Vim server has already been started. If
# not, it executes the command to launch the Vim server with the specified
# server name.
def start
rename_window
started? or cmd(*vim, '--servername', servername)
end
# The stop method sends a quit command to the remote editor.
#
# This method checks if the editor is currently running and, if so, sends a
# quit command to close all windows and terminate the editor session.
def stop
started? and edit_remote_send('<ESC>:qa<CR>')
end
# The activate method switches to the Vim editor window or opens a new one.
#
# This method checks if the Vim default arguments include the '-g' flag to determine
# whether to open a new buffer in the current window or switch to an
# existing Vim pane. When the '-g' flag is present, it creates a temporary
# file and then closes it. Otherwise, it identifies the appropriate tmux
# pane running an editor process and switches to it.
def activate
if Array(config.vim_default_args).include?('-g')
edit_remote("stupid_trick#{rand}")
sleep pause_duration
edit_remote_send('<ESC>:bw<CR>')
else
pstree = PSTree.new
switch_to_index =
`tmux list-panes -F '\#{pane_pid} \#{pane_index}'`.lines.find { |l|
pid, index = l.split(' ')
pid = pid.to_i
if pstree.find { |ps| ps.pid != $$ && ps.ppid == pid && ps.cmd =~ %r(/edit( |$)) }
break index.to_i
end
}
switch_to_index and system "tmux select-pane -t #{switch_to_index}"
end
end
# The serverlist method retrieves a list of available Vim server names.
#
# This method executes the Vim command to list all active servers and
# returns the results as an array of server names.
#
# @return [ Array<String> ] an array of Vim server names currently available
def serverlist
`#{vim.map(&:inspect) * ' '} --serverlist`.split
end
# The started? method checks whether a server with the given name is
# currently running.
#
# This method verifies the presence of a server in the list of active
# servers by checking if the server name exists within the serverlist.
#
# @param name [ String ] the name of the server to check for
#
# @return [ TrueClass, FalseClass ] true if the server is running, false otherwise
def started?(name = servername)
serverlist.member?(name)
end
# The edit_remote method executes a remote Vim command with the specified
# arguments.
#
# This method prepares a command to communicate with a running Vim server
# instance, allowing for remote execution of Vim commands without directly
# interacting with the terminal. It ensures the window is renamed before
# sending the command and constructs the appropriate command line arguments
# for the Vim server interface.
#
# @param args [ Array ] the arguments to be passed to the remote Vim command
def edit_remote(*args)
rename_window
cmd(*vim, '--servername', servername, '--remote', *args)
end
# The edit_remote_wait method executes a command remotely and waits for its
# completion.
#
# This method sends a command to a remote server using the specified vim
# server connection, and blocks until the remote operation finishes
# executing.
#
# @param args [ Array ] the arguments to be passed to the remote command
def edit_remote_wait(*args)
rename_window
cmd(*vim, '--servername', servername, '--remote-wait', *args)
end
# The edit_remote_send method transmits a sequence of arguments to a remote
# Vim server for execution.
#
# This method prepares and sends commands to an already running Vim
# instance identified by its server name, allowing for remote control of
# the editor session. It ensures the window is properly named before
# sending the command, and uses the configured Vim executable along with
# its remote communication flags.
#
# @param args [ Array<String> ] the arguments to be sent to the remote Vim server
def edit_remote_send(*args)
rename_window
cmd(*vim, '--servername', servername, '--remote-send', *args)
end
# The edit_remote_file method delegates to either edit_remote_wait or
# edit_remote based on the wait? condition.
#
# This method determines whether to execute file editing operations with
# waiting for completion or without waiting, depending on the result of the
# wait? check.
#
# @param filenames [ Array<String> ] an array of filenames to be processed
def edit_remote_file(*filenames)
if wait?
edit_remote_wait(*filenames)
else
edit_remote(*filenames)
end
end
end
end