581 lines
15 KiB
Ruby
Executable file
581 lines
15 KiB
Ruby
Executable file
#!/usr/bin/env ruby
|
|
|
|
# == Diakonos
|
|
#
|
|
# A Linux console text editor for the masses.
|
|
# :title: Diakonos
|
|
#
|
|
# Author:: Pistos (irc.freenode.net)
|
|
# http://diakonos.pist0s.ca/
|
|
# Copyright (c) 2004-2011 Pistos
|
|
#
|
|
# This software is released under the MIT licence.
|
|
# See the LICENCE file included with this program, or
|
|
# http://www.opensource.org/licenses/mit-license.php
|
|
#
|
|
require 'curses'
|
|
require 'open3'
|
|
require 'thread'
|
|
require 'English'
|
|
require 'set'
|
|
require 'yaml'
|
|
require 'digest/md5'
|
|
require 'timeout'
|
|
|
|
require 'diakonos/core-ext/object'
|
|
require 'diakonos/core-ext/enumerable'
|
|
require 'diakonos/core-ext/regexp'
|
|
require 'diakonos/core-ext/hash'
|
|
require 'diakonos/core-ext/string'
|
|
require 'diakonos/core-ext/fixnum'
|
|
require 'diakonos/core-ext/bignum'
|
|
require 'diakonos/buffer-hash'
|
|
require 'diakonos/sized-array'
|
|
|
|
require 'diakonos/version'
|
|
require 'diakonos/installation'
|
|
|
|
require 'diakonos/about'
|
|
require 'diakonos/buffer-management'
|
|
require 'diakonos/config'
|
|
require 'diakonos/cursor'
|
|
require 'diakonos/functions'
|
|
require 'diakonos/functions/basics'
|
|
require 'diakonos/functions/bookmarking'
|
|
require 'diakonos/functions/buffers'
|
|
require 'diakonos/functions/clipboard'
|
|
require 'diakonos/functions/cursor'
|
|
require 'diakonos/functions/grepping'
|
|
require 'diakonos/functions/indentation'
|
|
require 'diakonos/functions/readline'
|
|
require 'diakonos/functions/search'
|
|
require 'diakonos/functions/selection'
|
|
require 'diakonos/functions/sessions'
|
|
require 'diakonos/functions/shell'
|
|
require 'diakonos/functions/tags'
|
|
require 'diakonos/functions/text-manipulation'
|
|
require 'diakonos/functions-deprecated'
|
|
require 'diakonos/help'
|
|
require 'diakonos/display'
|
|
require 'diakonos/display/format'
|
|
require 'diakonos/grep'
|
|
require 'diakonos/hooks'
|
|
require 'diakonos/interaction'
|
|
require 'diakonos/keying'
|
|
require 'diakonos/logging'
|
|
require 'diakonos/list'
|
|
require 'diakonos/search'
|
|
require 'diakonos/sessions'
|
|
|
|
require 'diakonos/text-mark'
|
|
require 'diakonos/bookmark'
|
|
require 'diakonos/ctag'
|
|
require 'diakonos/finding'
|
|
require 'diakonos/buffer'
|
|
require 'diakonos/buffer/bookmarking'
|
|
require 'diakonos/buffer/cursor'
|
|
require 'diakonos/buffer/delete'
|
|
require 'diakonos/buffer/display'
|
|
require 'diakonos/buffer/indentation'
|
|
require 'diakonos/buffer/file'
|
|
require 'diakonos/buffer/searching'
|
|
require 'diakonos/buffer/selection'
|
|
require 'diakonos/buffer/undo'
|
|
require 'diakonos/clipboard'
|
|
require 'diakonos/clipboard-klipper'
|
|
require 'diakonos/clipboard-klipper-dbus'
|
|
require 'diakonos/clipboard-xclip'
|
|
require 'diakonos/extension'
|
|
require 'diakonos/extension-set'
|
|
require 'diakonos/key-map'
|
|
require 'diakonos/mode'
|
|
require 'diakonos/readline'
|
|
require 'diakonos/readline/functions'
|
|
|
|
require 'diakonos/vendor/fuzzy_file_finder'
|
|
|
|
|
|
module Diakonos
|
|
|
|
DONT_ADJUST_ROW = false
|
|
ADJUST_ROW = true
|
|
PROMPT_OVERWRITE = true
|
|
DONT_PROMPT_OVERWRITE = false
|
|
QUIET = true
|
|
NOISY = false
|
|
AFTER_CHAR = true
|
|
ON_CHAR = false
|
|
INCLUSIVE = true
|
|
NOT_INCLUSIVE = false
|
|
|
|
DEFAULT_TAB_SIZE = 8
|
|
|
|
FORCE_REVERT = true
|
|
ASK_REVERT = false
|
|
|
|
ASK_REPLACEMENT = true
|
|
|
|
CASE_SENSITIVE = true
|
|
CASE_INSENSITIVE = false
|
|
|
|
LANG_TEXT = 'text'
|
|
|
|
NUM_LAST_COMMANDS = 2
|
|
|
|
class Diakonos
|
|
|
|
attr_reader :diakonos_home, :script_dir, :clipboard,
|
|
:list_filename, :hooks, :indenters, :indenters_next_line, :unindenters, :closers,
|
|
:last_commands, :there_was_non_movement, :do_display, :testing
|
|
|
|
include ::Diakonos::Functions
|
|
|
|
def initialize( argv = [] )
|
|
@diakonos_home = File.expand_path( ( ENV[ 'HOME' ] || '' ) + '/.diakonos' )
|
|
mkdir @diakonos_home
|
|
@script_dir = "#{@diakonos_home}/scripts"
|
|
mkdir @script_dir
|
|
@extensions = ExtensionSet.new( File.join( @diakonos_home, 'extensions' ) )
|
|
initialize_session
|
|
|
|
@files = Array.new
|
|
@read_only_files = Array.new
|
|
@config_filename = nil
|
|
parse_options argv
|
|
|
|
init_help
|
|
|
|
@debug = File.new( File.join( @diakonos_home, 'debug.log' ), 'w' )
|
|
@list_filename = File.join( @diakonos_home, 'listing.txt' )
|
|
@diff_filename = File.join( @diakonos_home, 'text.diff' )
|
|
@help_filename = File.join( @help_dir, 'about-help.dhf' )
|
|
@error_filename = File.join( @diakonos_home, 'diakonos.err' )
|
|
@about_filename = File.join( @diakonos_home, 'about.dhf' )
|
|
|
|
@win_main = nil
|
|
@win_context = nil
|
|
@win_status = nil
|
|
@win_interaction = nil
|
|
@win_line_numbers = nil
|
|
@buffers = Array.new
|
|
|
|
load_configuration
|
|
|
|
@quitting = false
|
|
|
|
@x = 0
|
|
@y = 0
|
|
|
|
@buffer_stack = Array.new
|
|
@buffer_current = nil
|
|
|
|
@cursor_stack = Array.new
|
|
@cursor_stack_pointer = nil
|
|
|
|
@bookmarks = Hash.new
|
|
|
|
@macro_history = nil
|
|
@macro_input_history = nil
|
|
@macros = Hash.new
|
|
@last_commands = SizedArray.new( NUM_LAST_COMMANDS )
|
|
@playing_macro = false
|
|
|
|
@display_mutex = Mutex.new
|
|
@display_queue_mutex = Mutex.new
|
|
@display_queue = nil
|
|
@do_display = true
|
|
@iline_mutex = Mutex.new
|
|
|
|
@tag_stack = Array.new
|
|
@last_search_regexps = nil
|
|
@iterated_choice = nil
|
|
@choice_iterations = 0
|
|
@there_was_non_movement = false
|
|
@status_vars = Hash.new
|
|
|
|
# Readline histories
|
|
@rlh_general = Array.new
|
|
@rlh_files = Array.new
|
|
@rlh_search = Array.new
|
|
@rlh_shell = Array.new
|
|
@rlh_help = Array.new
|
|
@rlh_sessions = Array.new
|
|
|
|
@hooks = {
|
|
:after_buffer_switch => [],
|
|
:after_open => [],
|
|
:after_save => [],
|
|
:after_startup => [],
|
|
}
|
|
end
|
|
|
|
def mkdir( dir )
|
|
if ! FileTest.exists?( dir )
|
|
Dir.mkdir dir
|
|
end
|
|
end
|
|
|
|
def parse_options( argv )
|
|
@post_load_script = ""
|
|
while argv.length > 0
|
|
arg = argv.shift
|
|
case arg
|
|
when '-c', '--config'
|
|
@config_filename = argv.shift
|
|
if @config_filename.nil?
|
|
print_usage
|
|
exit 1
|
|
end
|
|
when '-e', '--execute'
|
|
post_load_script = argv.shift
|
|
if post_load_script.nil?
|
|
print_usage
|
|
exit 1
|
|
else
|
|
@post_load_script << "\n#{post_load_script}"
|
|
end
|
|
when '-h', '--help'
|
|
print_usage
|
|
exit 1
|
|
when '-m', '--open-matching'
|
|
regexp = argv.shift
|
|
files = `egrep -rl '#{regexp}' *`.split( /\n/ )
|
|
if files.any?
|
|
@files.concat( files.map { |f| session_file_hash_for f } )
|
|
script = "\nfind 'down', CASE_SENSITIVE, '#{regexp}'"
|
|
@post_load_script << script
|
|
end
|
|
when '-ro'
|
|
filename = argv.shift
|
|
if filename.nil?
|
|
print_usage
|
|
exit 1
|
|
else
|
|
h = session_file_hash_for( filename )
|
|
h[ 'read_only' ] = true
|
|
@read_only_files.push h
|
|
end
|
|
when '-s', '--load-session'
|
|
@session_to_load = session_filepath_for( argv.shift )
|
|
when '--test', '--testing'
|
|
@testing = true
|
|
when '--uninstall'
|
|
uninstall
|
|
when '--uninstall-without-confirmation'
|
|
uninstall false
|
|
when '--version'
|
|
puts "Diakonos #{::Diakonos::VERSION} (#{::Diakonos::LAST_MODIFIED})"
|
|
exit 0
|
|
else
|
|
# a name of a file to open
|
|
@files.push session_file_hash_for( arg )
|
|
end
|
|
end
|
|
end
|
|
|
|
def print_usage
|
|
puts "Usage: #{$0} [options] [file] [file...]"
|
|
puts "\t--help\tDisplay usage"
|
|
puts "\t-c <config file>\tLoad this config file instead of ~/.diakonos/diakonos.conf"
|
|
puts "\t-e, --execute <Ruby code>\tExecute Ruby code (such as Diakonos commands) after startup"
|
|
puts "\t-m, --open-matching <regular expression>\tOpen all matching files under current directory"
|
|
puts "\t-ro <file>\tLoad file as read-only"
|
|
puts "\t-s, --load-session <session identifier>\tLoad a session"
|
|
puts "\t--uninstall[-without-confirmation]\tUninstall Diakonos"
|
|
end
|
|
|
|
def clear_non_movement_flag
|
|
@there_was_non_movement = false
|
|
end
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
def start
|
|
require 'diakonos/window'
|
|
|
|
initialize_display
|
|
|
|
@files.each do |file|
|
|
@buffers << Buffer.new( file )
|
|
end
|
|
@files = []
|
|
@read_only_files.each do |file|
|
|
@buffers << Buffer.new( file )
|
|
end
|
|
if ! @testing
|
|
session_buffers = session_startup
|
|
end
|
|
session_buffer_number = @session[ 'buffer_current' ] || 1
|
|
@files.each do |file_info|
|
|
@buffers << Buffer.new( file_info )
|
|
end
|
|
if @buffers.empty?
|
|
@buffers << Buffer.new
|
|
end
|
|
|
|
@buffers.each do |buffer|
|
|
buffer.reset_display
|
|
end
|
|
|
|
if ENV[ 'COLORTERM' ] == 'gnome-terminal'
|
|
help_key = 'Shift-F1'
|
|
else
|
|
help_key = 'F1'
|
|
end
|
|
set_iline "Diakonos #{VERSION} (#{LAST_MODIFIED}) #{help_key} for help F12 to configure Ctrl-Q to quit"
|
|
|
|
scripts = @extensions.scripts + Dir[ "#{@script_dir}/*" ]
|
|
scripts.each do |script|
|
|
begin
|
|
require script
|
|
rescue Exception => e
|
|
show_exception(
|
|
e,
|
|
[
|
|
"There is a syntax error in the script.",
|
|
"An invalid hook name was used."
|
|
]
|
|
)
|
|
end
|
|
end
|
|
|
|
@hooks.each do |hook_name, hook|
|
|
hook.sort { |a,b| a[ :priority ] <=> b[ :priority ] }
|
|
end
|
|
|
|
if session_buffers
|
|
session_buffers.each do |buffer|
|
|
close_file buffer
|
|
end
|
|
end
|
|
|
|
if @post_load_script
|
|
eval @post_load_script
|
|
end
|
|
|
|
run_hook_procs :after_startup
|
|
if ! switch_to_buffer_number( session_buffer_number )
|
|
debug_log "Failed to switch to buffer #{session_buffer_number.inspect}"
|
|
switch_to_buffer_number 1
|
|
end
|
|
|
|
@buffers.each do |b|
|
|
run_hook_procs :after_open, b
|
|
b.cursor_to( b.last_row, b.last_col, Buffer::DONT_DISPLAY )
|
|
end
|
|
|
|
if @config_problems.any?
|
|
File.open( @error_filename, "w" ) do |f|
|
|
f.puts "There are #{@config_problems.size} problems with the configuration file(s):"
|
|
@config_problems.each do |p|
|
|
f.puts p
|
|
end
|
|
end
|
|
open_file @error_filename
|
|
end
|
|
|
|
if ! @settings[ 'suppress_welcome' ]
|
|
open_file "#{@help_dir}/welcome.dhf"
|
|
else
|
|
@buffer_current.seek /<<<</
|
|
end
|
|
|
|
begin
|
|
# Main keyboard loop.
|
|
while ! @quitting
|
|
process_keystroke
|
|
@win_main.refresh
|
|
end
|
|
rescue SignalException => e
|
|
debug_log "Terminated by signal (#{e.message})"
|
|
end
|
|
|
|
cleanup_display
|
|
cleanup_session
|
|
|
|
@debug.close
|
|
end
|
|
|
|
def uninstall( confirm = true )
|
|
inst = ::Diakonos::INSTALL_SETTINGS[ :installed ]
|
|
|
|
if confirm
|
|
puts inst[ :files ].sort.join( "\n" )
|
|
puts
|
|
puts inst[ :dirs ].sort.map { |d| "#{d}/" }.join( "\n" )
|
|
puts
|
|
puts "The above files will be removed. The above directories will be removed if they are empty. Proceed? (y/n)"
|
|
answer = $stdin.gets
|
|
case answer
|
|
when /^y/i
|
|
puts "Deleting..."
|
|
else
|
|
puts "Uninstallation aborted."
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
require 'fileutils'
|
|
inst[ :files ].each do |f|
|
|
FileUtils.rm f
|
|
end
|
|
inst[ :dirs ].sort { |d1,d2| d2.length <=> d1.length }.each do |d|
|
|
begin
|
|
FileUtils.rmdir d
|
|
rescue Errno::ENOTEMPTY
|
|
end
|
|
if File.exists? d
|
|
$stderr.puts "(#{d} not removed)"
|
|
end
|
|
end
|
|
|
|
exit 0
|
|
end
|
|
|
|
def get_language_from_name( name )
|
|
retval = nil
|
|
@filemasks.each do |language,filemask|
|
|
if name =~ filemask
|
|
retval = language
|
|
break
|
|
end
|
|
end
|
|
retval
|
|
end
|
|
|
|
def get_language_from_shabang( first_line )
|
|
retval = nil
|
|
@bangmasks.each do |language,bangmask|
|
|
if first_line =~ bangmask
|
|
retval = language
|
|
break
|
|
end
|
|
end
|
|
retval
|
|
end
|
|
|
|
def show_exception( e, probable_causes = [ "Unknown" ] )
|
|
begin
|
|
File.open( @error_filename, "w" ) do |f|
|
|
f.puts "Diakonos Error:"
|
|
f.puts
|
|
f.puts "#{e.class}: #{e.message}"
|
|
f.puts
|
|
f.puts "Probable Causes:"
|
|
f.puts
|
|
probable_causes.each do |pc|
|
|
f.puts "- #{pc}"
|
|
end
|
|
f.puts
|
|
f.puts "----------------------------------------------------"
|
|
f.puts "If you can reproduce this error, please report it at"
|
|
f.puts "https://github.com/Pistos/diakonos/issues !"
|
|
f.puts "----------------------------------------------------"
|
|
f.puts e.backtrace
|
|
end
|
|
open_file @error_filename
|
|
rescue Exception => e2
|
|
debug_log "EXCEPTION: #{e.message}"
|
|
debug_log "\t#{e.backtrace}"
|
|
end
|
|
end
|
|
|
|
def start_recording_macro( name = nil )
|
|
return if @macro_history
|
|
@macro_name = name
|
|
@macro_history = Array.new
|
|
@macro_input_history = Array.new
|
|
set_iline "Started macro recording."
|
|
end
|
|
|
|
def stop_recording_macro
|
|
@macro_history.pop # Remove the stop_recording_macro command itself
|
|
@macros[ @macro_name ] = [ @macro_history, @macro_input_history ]
|
|
@macro_history = nil
|
|
@macro_input_history = nil
|
|
set_iline "Stopped macro recording."
|
|
end
|
|
|
|
def load_tags
|
|
@tags = Hash.new
|
|
if buffer_current && buffer_current.name
|
|
path = File.expand_path( File.dirname( buffer_current.name ) )
|
|
tagfile = path + "/tags"
|
|
else
|
|
tagfile = "./tags"
|
|
end
|
|
if FileTest.exists? tagfile
|
|
IO.foreach( tagfile ) do |line_|
|
|
line = line_.chomp
|
|
# <tagname>\t<filepath>\t<line number or regexp>\t<kind of tag>
|
|
tag, file, command, kind, rest = line.split( /\t/ )
|
|
command.gsub!( /;"$/, "" )
|
|
if command =~ /^\/.*\/$/
|
|
command = command[ 1...-1 ]
|
|
end
|
|
@tags[ tag ] ||= Array.new
|
|
@tags[ tag ].push CTag.new( file, command, kind, rest )
|
|
end
|
|
else
|
|
set_iline "(tags file not found)"
|
|
end
|
|
end
|
|
|
|
def escape_quotes( str )
|
|
temp = ''
|
|
str.each_byte do |b|
|
|
if b == 39
|
|
temp << 39
|
|
temp << 92
|
|
temp << 39
|
|
end
|
|
temp << b
|
|
end
|
|
temp
|
|
end
|
|
|
|
def direction_of( str, default = :down )
|
|
case str
|
|
when "up"
|
|
:up
|
|
when /other/
|
|
:opposite
|
|
when "down"
|
|
:down
|
|
when "forward"
|
|
:forward
|
|
when "backward"
|
|
:backward
|
|
else
|
|
default
|
|
end
|
|
end
|
|
|
|
# @return [Array] the filename and line number parsed
|
|
def parse_filename_and_line_number( s )
|
|
if(
|
|
# Ruby
|
|
s =~ /from (.+):(\d+)/ ||
|
|
# Python
|
|
s =~ /File "(.+)", line (\d+)/ ||
|
|
# Perl
|
|
s =~ /at (.+) line (\d+)/ ||
|
|
# generic
|
|
s =~ /^(.+):(\d+)/
|
|
)
|
|
[ $1, ( $2.to_i - 1 ) ]
|
|
else
|
|
[ s, nil ]
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
::Diakonos.check_ruby_version
|
|
|
|
if __FILE__ == $PROGRAM_NAME
|
|
$diakonos = Diakonos::Diakonos.new( ARGV )
|
|
$diakonos.start
|
|
end
|