sup 0.12.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (56) hide show
  1. data.tar.gz.sig +1 -0
  2. data/CONTRIBUTORS +25 -12
  3. data/History.txt +6 -1
  4. data/README.md +70 -0
  5. data/ReleaseNotes +5 -0
  6. data/bin/sup +22 -15
  7. data/bin/sup-add +3 -3
  8. data/bin/sup-config +3 -4
  9. data/bin/sup-import-dump +1 -1
  10. data/bin/sup-sync +1 -1
  11. data/bin/sup-sync-back +2 -2
  12. data/bin/sup-tweak-labels +1 -1
  13. data/lib/sup.rb +39 -23
  14. data/lib/sup/account.rb +4 -0
  15. data/lib/sup/buffer.rb +4 -7
  16. data/lib/sup/colormap.rb +10 -2
  17. data/lib/sup/contact.rb +11 -5
  18. data/lib/sup/crypto.rb +278 -101
  19. data/lib/sup/draft.rb +3 -2
  20. data/lib/sup/horizontal-selector.rb +5 -2
  21. data/lib/sup/index.rb +47 -42
  22. data/lib/sup/label.rb +1 -1
  23. data/lib/sup/message-chunks.rb +4 -2
  24. data/lib/sup/message.rb +14 -3
  25. data/lib/sup/modes/buffer-list-mode.rb +1 -1
  26. data/lib/sup/modes/compose-mode.rb +1 -1
  27. data/lib/sup/modes/contact-list-mode.rb +2 -2
  28. data/lib/sup/modes/edit-message-async-mode.rb +109 -0
  29. data/lib/sup/modes/edit-message-mode.rb +148 -16
  30. data/lib/sup/modes/file-browser-mode.rb +2 -2
  31. data/lib/sup/modes/forward-mode.rb +4 -4
  32. data/lib/sup/modes/line-cursor-mode.rb +2 -2
  33. data/lib/sup/modes/reply-mode.rb +34 -30
  34. data/lib/sup/modes/resume-mode.rb +4 -1
  35. data/lib/sup/modes/scroll-mode.rb +8 -6
  36. data/lib/sup/modes/text-mode.rb +1 -1
  37. data/lib/sup/modes/thread-index-mode.rb +44 -25
  38. data/lib/sup/modes/thread-view-mode.rb +26 -24
  39. data/lib/sup/person.rb +18 -7
  40. data/lib/sup/poll.rb +1 -1
  41. data/lib/sup/rfc2047.rb +1 -1
  42. data/lib/sup/sent.rb +2 -2
  43. data/lib/sup/source.rb +1 -1
  44. data/lib/sup/textfield.rb +38 -1
  45. data/lib/sup/thread.rb +1 -1
  46. data/lib/sup/time.rb +83 -0
  47. data/lib/sup/util.rb +38 -74
  48. data/lib/sup/version.rb +3 -0
  49. metadata +333 -168
  50. metadata.gz.sig +0 -0
  51. data/README.txt +0 -128
  52. data/bin/sup-cmd +0 -138
  53. data/bin/sup-server +0 -44
  54. data/lib/sup/client.rb +0 -92
  55. data/lib/sup/protocol.rb +0 -161
  56. data/lib/sup/server.rb +0 -116
data/lib/sup/draft.rb CHANGED
@@ -23,7 +23,7 @@ class DraftManager
23
23
  def discard m
24
24
  raise ArgumentError, "not a draft: source id #{m.source.id.inspect}, should be #{DraftManager.source_id.inspect} for #{m.id.inspect}" unless m.source.id.to_i == DraftManager.source_id
25
25
  Index.delete m.id
26
- File.delete @source.fn_for_offset(m.source_info)
26
+ File.delete @source.fn_for_offset(m.source_info) rescue Errono::ENOENT
27
27
  UpdateManager.relay self, :single_message_deleted, m
28
28
  end
29
29
  end
@@ -70,8 +70,9 @@ class DraftLoader < Source
70
70
  def load_header offset
71
71
  File.open(fn_for_offset(offset)) { |f| parse_raw_email_header f }
72
72
  end
73
-
73
+
74
74
  def load_message offset
75
+ raise SourceError, "Draft not found" unless File.exists? fn_for_offset(offset)
75
76
  File.open fn_for_offset(offset) do |f|
76
77
  RMail::Mailbox::MBoxReader.new(f).each_message do |input|
77
78
  return RMail::Parser.read(input)
@@ -1,7 +1,7 @@
1
1
  module Redwood
2
2
 
3
3
  class HorizontalSelector
4
- attr_accessor :label
4
+ attr_accessor :label, :changed_by_user
5
5
 
6
6
  def initialize label, vals, labels, base_color=:horizontal_selector_unselected_color, selected_color=:horizontal_selector_selected_color
7
7
  @label = label
@@ -10,6 +10,7 @@ class HorizontalSelector
10
10
  @base_color = base_color
11
11
  @selected_color = selected_color
12
12
  @selection = 0
13
+ @changed_by_user = false
13
14
  end
14
15
 
15
16
  def set_to val; @selection = @vals.index(val) end
@@ -24,7 +25,7 @@ class HorizontalSelector
24
25
  "#{@label} "
25
26
  end
26
27
 
27
- [[@base_color, label]] +
28
+ [[@base_color, label]] +
28
29
  (0 ... @labels.length).inject([]) do |array, i|
29
30
  array + [
30
31
  if i == @selection
@@ -37,10 +38,12 @@ class HorizontalSelector
37
38
 
38
39
  def roll_left
39
40
  @selection = (@selection - 1) % @labels.length
41
+ @changed_by_user = true
40
42
  end
41
43
 
42
44
  def roll_right
43
45
  @selection = (@selection + 1) % @labels.length
46
+ @changed_by_user = true
44
47
  end
45
48
  end
46
49
 
data/lib/sup/index.rb CHANGED
@@ -13,6 +13,10 @@ rescue LoadError => e
13
13
  $have_chronic = false
14
14
  end
15
15
 
16
+ if ([Xapian.major_version, Xapian.minor_version, Xapian.revision] <=> [1,2,1]) < 0
17
+ fail "Xapian version 1.2.1 or higher required"
18
+ end
19
+
16
20
  module Redwood
17
21
 
18
22
  # This index implementation uses Xapian for searching and storage. It
@@ -21,7 +25,6 @@ module Redwood
21
25
  class Index
22
26
  include InteractiveLock
23
27
 
24
- STEM_LANGUAGE = "english"
25
28
  INDEX_VERSION = '4'
26
29
 
27
30
  ## dates are converted to integers for xapian, and are used for document ids,
@@ -206,7 +209,9 @@ EOS
206
209
  :labels => entry[:labels],
207
210
  :snippet => entry[:snippet]
208
211
 
209
- mk_person = lambda { |x| Person.new(*x.reverse!) }
212
+ # Try to find person from contacts before falling back to
213
+ # generating it from the address.
214
+ mk_person = lambda { |x| Person.from_name_and_email(*x.reverse!) }
210
215
  entry[:from] = mk_person[entry[:from]]
211
216
  entry[:to].map!(&mk_person)
212
217
  entry[:cc].map!(&mk_person)
@@ -231,7 +236,7 @@ EOS
231
236
  m = b.call
232
237
  ([m.from]+m.to+m.cc+m.bcc).compact.each { |p| contacts << [p.name, p.email] }
233
238
  end
234
- contacts.to_a.compact.map { |n,e| Person.new n, e }[0...num]
239
+ contacts.to_a.compact[0...num].map { |n,e| Person.from_name_and_email n, e }
235
240
  end
236
241
 
237
242
  ## Yield each message-id matching query
@@ -422,12 +427,12 @@ EOS
422
427
 
423
428
  qp = Xapian::QueryParser.new
424
429
  qp.database = @xapian
425
- qp.stemmer = Xapian::Stem.new(STEM_LANGUAGE)
430
+ qp.stemmer = Xapian::Stem.new($config[:stem_language])
426
431
  qp.stemming_strategy = Xapian::QueryParser::STEM_SOME
427
432
  qp.default_op = Xapian::Query::OP_AND
428
433
  qp.add_valuerangeprocessor(Xapian::NumberValueRangeProcessor.new(DATE_VALUENO, 'date:', true))
429
- NORMAL_PREFIX.each { |k,vs| vs.each { |v| qp.add_prefix k, v } }
430
- BOOLEAN_PREFIX.each { |k,vs| vs.each { |v| qp.add_boolean_prefix k, v } }
434
+ NORMAL_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_prefix k, v } }
435
+ BOOLEAN_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_boolean_prefix k, v, info[:exclusive] } }
431
436
 
432
437
  begin
433
438
  xapian_query = qp.parse_query(subs, Xapian::QueryParser::FLAG_PHRASE|Xapian::QueryParser::FLAG_BOOLEAN|Xapian::QueryParser::FLAG_LOVEHATE|Xapian::QueryParser::FLAG_WILDCARD)
@@ -478,31 +483,31 @@ EOS
478
483
 
479
484
  # Stemmed
480
485
  NORMAL_PREFIX = {
481
- 'subject' => 'S',
482
- 'body' => 'B',
483
- 'from_name' => 'FN',
484
- 'to_name' => 'TN',
485
- 'name' => %w(FN TN),
486
- 'attachment' => 'A',
487
- 'email_text' => 'E',
488
- '' => %w(S B FN TN A E),
486
+ 'subject' => {:prefix => 'S', :exclusive => false},
487
+ 'body' => {:prefix => 'B', :exclusive => false},
488
+ 'from_name' => {:prefix => 'FN', :exclusive => false},
489
+ 'to_name' => {:prefix => 'TN', :exclusive => false},
490
+ 'name' => {:prefix => %w(FN TN), :exclusive => false},
491
+ 'attachment' => {:prefix => 'A', :exclusive => false},
492
+ 'email_text' => {:prefix => 'E', :exclusive => false},
493
+ '' => {:prefix => %w(S B FN TN A E), :exclusive => false},
489
494
  }
490
495
 
491
496
  # Unstemmed
492
497
  BOOLEAN_PREFIX = {
493
- 'type' => 'K',
494
- 'from_email' => 'FE',
495
- 'to_email' => 'TE',
496
- 'email' => %w(FE TE),
497
- 'date' => 'D',
498
- 'label' => 'L',
499
- 'source_id' => 'I',
500
- 'attachment_extension' => 'O',
501
- 'msgid' => 'Q',
502
- 'id' => 'Q',
503
- 'thread' => 'H',
504
- 'ref' => 'R',
505
- 'location' => 'J',
498
+ 'type' => {:prefix => 'K', :exclusive => true},
499
+ 'from_email' => {:prefix => 'FE', :exclusive => false},
500
+ 'to_email' => {:prefix => 'TE', :exclusive => false},
501
+ 'email' => {:prefix => %w(FE TE), :exclusive => false},
502
+ 'date' => {:prefix => 'D', :exclusive => true},
503
+ 'label' => {:prefix => 'L', :exclusive => false},
504
+ 'source_id' => {:prefix => 'I', :exclusive => true},
505
+ 'attachment_extension' => {:prefix => 'O', :exclusive => false},
506
+ 'msgid' => {:prefix => 'Q', :exclusive => true},
507
+ 'id' => {:prefix => 'Q', :exclusive => true},
508
+ 'thread' => {:prefix => 'H', :exclusive => false},
509
+ 'ref' => {:prefix => 'R', :exclusive => false},
510
+ 'location' => {:prefix => 'J', :exclusive => false},
506
511
  }
507
512
 
508
513
  PREFIX = NORMAL_PREFIX.merge BOOLEAN_PREFIX
@@ -668,8 +673,8 @@ EOS
668
673
  # Person names are indexed with several prefixes
669
674
  person_termer = lambda do |d|
670
675
  lambda do |p|
671
- doc.index_text p.name, PREFIX["#{d}_name"] if p.name
672
- doc.index_text p.email, PREFIX['email_text']
676
+ doc.index_text p.name, PREFIX["#{d}_name"][:prefix] if p.name
677
+ doc.index_text p.email, PREFIX['email_text'][:prefix]
673
678
  doc.add_term mkterm(:email, d, p.email)
674
679
  end
675
680
  end
@@ -680,9 +685,9 @@ EOS
680
685
  # Full text search content
681
686
  subject_text = m.indexable_subject
682
687
  body_text = m.indexable_body
683
- doc.index_text subject_text, PREFIX['subject']
684
- doc.index_text body_text, PREFIX['body']
685
- m.attachments.each { |a| doc.index_text a, PREFIX['attachment'] }
688
+ doc.index_text subject_text, PREFIX['subject'][:prefix]
689
+ doc.index_text body_text, PREFIX['body'][:prefix]
690
+ m.attachments.each { |a| doc.index_text a, PREFIX['attachment'][:prefix] }
686
691
 
687
692
  # Miscellaneous terms
688
693
  doc.add_term mkterm(:date, m.date) if m.date
@@ -760,25 +765,25 @@ EOS
760
765
  def mkterm type, *args
761
766
  case type
762
767
  when :label
763
- PREFIX['label'] + args[0].to_s.downcase
768
+ PREFIX['label'][:prefix] + args[0].to_s.downcase
764
769
  when :type
765
- PREFIX['type'] + args[0].to_s.downcase
770
+ PREFIX['type'][:prefix] + args[0].to_s.downcase
766
771
  when :date
767
- PREFIX['date'] + args[0].getutc.strftime("%Y%m%d%H%M%S")
772
+ PREFIX['date'][:prefix] + args[0].getutc.strftime("%Y%m%d%H%M%S")
768
773
  when :email
769
774
  case args[0]
770
- when :from then PREFIX['from_email']
771
- when :to then PREFIX['to_email']
775
+ when :from then PREFIX['from_email'][:prefix]
776
+ when :to then PREFIX['to_email'][:prefix]
772
777
  else raise "Invalid email term type #{args[0]}"
773
778
  end + args[1].to_s.downcase
774
779
  when :source_id
775
- PREFIX['source_id'] + args[0].to_s.downcase
780
+ PREFIX['source_id'][:prefix] + args[0].to_s.downcase
776
781
  when :location
777
- PREFIX['location'] + [args[0]].pack('n') + args[1].to_s
782
+ PREFIX['location'][:prefix] + [args[0]].pack('n') + args[1].to_s
778
783
  when :attachment_extension
779
- PREFIX['attachment_extension'] + args[0].to_s.downcase
784
+ PREFIX['attachment_extension'][:prefix] + args[0].to_s.downcase
780
785
  when :msgid, :ref, :thread
781
- PREFIX[type.to_s] + args[0][0...(MAX_TERM_LENGTH-1)]
786
+ PREFIX[type.to_s][:prefix] + args[0][0...(MAX_TERM_LENGTH-1)]
782
787
  else
783
788
  raise "Invalid term type #{type}"
784
789
  end
@@ -798,7 +803,7 @@ class Xapian::Document
798
803
 
799
804
  def index_text text, prefix, weight=1
800
805
  term_generator = Xapian::TermGenerator.new
801
- term_generator.stemmer = Xapian::Stem.new(Redwood::Index::STEM_LANGUAGE)
806
+ term_generator.stemmer = Xapian::Stem.new($config[:stem_language])
802
807
  term_generator.document = self
803
808
  term_generator.index_text text, weight, prefix
804
809
  end
data/lib/sup/label.rb CHANGED
@@ -12,7 +12,7 @@ class LabelManager
12
12
 
13
13
  def initialize fn
14
14
  @fn = fn
15
- labels =
15
+ labels =
16
16
  if File.exists? fn
17
17
  IO.readlines(fn).map { |x| x.chomp.intern }
18
18
  else
@@ -1,4 +1,5 @@
1
1
  require 'tempfile'
2
+ require 'rbconfig'
2
3
 
3
4
  ## Here we define all the "chunks" that a message is parsed
4
5
  ## into. Chunks are used by ThreadViewMode to render a message. Chunks
@@ -146,7 +147,7 @@ EOS
146
147
  def initial_state; :open end
147
148
  def viewable?; @lines.nil? end
148
149
  def view_default! path
149
- case Config::CONFIG['arch']
150
+ case RbConfig::CONFIG['arch']
150
151
  when /darwin/
151
152
  cmd = "open '#{path}'"
152
153
  else
@@ -198,7 +199,7 @@ EOS
198
199
  def initialize lines
199
200
  @lines = lines
200
201
  end
201
-
202
+
202
203
  def inlineable?; @lines.length == 1 end
203
204
  def quotable?; true end
204
205
  def expandable?; !inlineable? end
@@ -272,6 +273,7 @@ EOS
272
273
  def patina_color
273
274
  case status
274
275
  when :valid then :cryptosig_valid_color
276
+ when :valid_untrusted then :cryptosig_valid_untrusted_color
275
277
  when :invalid then :cryptosig_invalid_color
276
278
  else :cryptosig_unknown_color
277
279
  end
data/lib/sup/message.rb CHANGED
@@ -503,7 +503,7 @@ private
503
503
  filename = Rfc2047.decode_to $encoding, filename
504
504
  # add this to the attachments list if its not a generated html
505
505
  # attachment (should we allow images with generated names?).
506
- # Lowercase the filename because searches are easier that way
506
+ # Lowercase the filename because searches are easier that way
507
507
  @attachments.push filename.downcase unless filename =~ /^sup-attachment-/
508
508
  add_label :attachment unless filename =~ /^sup-attachment-/
509
509
  content_type = (m.header.content_type || "application/unknown").downcase # sometimes RubyMail gives us nil
@@ -590,9 +590,20 @@ private
590
590
  state = :text # one of :text, :quote, or :sig
591
591
  chunks = []
592
592
  chunk_lines = []
593
+ nextline_index = -1
593
594
 
594
595
  lines.each_with_index do |line, i|
595
- nextline = lines[(i + 1) ... lines.length].find { |l| l !~ /^\s*$/ } # skip blank lines
596
+ if i >= nextline_index
597
+ # look for next nonblank line only when needed to avoid O(n²)
598
+ # behavior on sequences of blank lines
599
+ if nextline_index = lines[(i+1)..-1].index { |l| l !~ /^\s*$/ } # skip blank lines
600
+ nextline_index += i + 1
601
+ nextline = lines[nextline_index]
602
+ else
603
+ nextline_index = lines.length
604
+ nextline = nil
605
+ end
606
+ end
596
607
 
597
608
  case state
598
609
  when :text
@@ -604,7 +615,7 @@ private
604
615
  ## like ":a:a:a:a:a" that occurred in certain emails.
605
616
  if line =~ QUOTE_PATTERN || (line =~ /:$/ && line =~ /\w/ && nextline =~ QUOTE_PATTERN)
606
617
  newstate = :quote
607
- elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE
618
+ elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE && !lines[(i+1)..-1].index { |l| l =~ /^-- $/ }
608
619
  newstate = :sig
609
620
  elsif line =~ BLOCK_QUOTE_PATTERN
610
621
  newstate = :block_quote
@@ -28,7 +28,7 @@ protected
28
28
  end
29
29
 
30
30
  def regen_text
31
- @bufs = BufferManager.buffers.reject { |name, buf| buf.mode == self }.sort_by { |name, buf| buf.atime }.reverse
31
+ @bufs = BufferManager.buffers.reject { |name, buf| buf.mode == self || buf.hidden? }.sort_by { |name, buf| buf.atime }.reverse
32
32
  width = @bufs.max_of { |name, buf| buf.mode.name.length }
33
33
  @text = @bufs.map do |name, buf|
34
34
  base_color = buf.system? ? :system_buf_color : :regular_buf_color
@@ -26,7 +26,7 @@ class ComposeMode < EditMessageMode
26
26
  cc = opts[:cc] || (BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc])
27
27
  bcc = opts[:bcc] || (BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc])
28
28
  subj = opts[:subj] || (BufferManager.ask(:subject, "Subject: ") or return if $config[:ask_for_subject])
29
-
29
+
30
30
  mode = ComposeMode.new :from => from, :to => to, :cc => cc, :bcc => bcc, :subj => subj
31
31
  BufferManager.spawn "New Message", mode
32
32
  mode.edit_message
@@ -89,7 +89,7 @@ class ContactListMode < LineCursorMode
89
89
  def search
90
90
  p = @contacts[curpos] or return
91
91
  multi_search [p]
92
- end
92
+ end
93
93
 
94
94
  def reload
95
95
  @tags.drop_all_tags
@@ -114,7 +114,7 @@ class ContactListMode < LineCursorMode
114
114
  @contacts = (@user_contacts + recentc).sort_by { |p| p.sort_by_me }.uniq
115
115
  end
116
116
  end
117
-
117
+
118
118
  protected
119
119
 
120
120
  def update
@@ -0,0 +1,109 @@
1
+ module Redwood
2
+
3
+ class EditMessageAsyncMode < LineCursorMode
4
+
5
+ HookManager.register "async-edit", <<EOS
6
+ Runs when 'H' is pressed in async edit mode. You can run whatever code
7
+ you want here - though the default case would be launching a text
8
+ editor. Your hook is assumed to not block, so you should use exec() or
9
+ fork() to launch the editor.
10
+
11
+ Once the hook has returned then sup will be responsive as usual. You will
12
+ still need to press 'E' to exit this buffer and send the message.
13
+
14
+ Variables:
15
+ file_path: The full path to the file containing the message to be edited.
16
+
17
+ Return value: None
18
+ EOS
19
+
20
+ register_keymap do |k|
21
+ k.add :run_async_hook, "Run the async-edit hook", 'H'
22
+ k.add :edit_finished, "Finished editing message", 'E'
23
+ k.add :path_to_clipboard, "Copy file path to the clipboard", :enter
24
+ end
25
+
26
+ def initialize parent_edit_mode, file_path, msg_subject
27
+ @parent_edit_mode = parent_edit_mode
28
+ @file_path = file_path
29
+ @orig_mtime = File.mtime @file_path
30
+
31
+ @text = ["ASYNC MESSAGE EDIT",
32
+ "", "Your message with subject:", msg_subject, "is saved in a file:", "", @file_path, "",
33
+ "You can edit your message in the editor of your choice and continue to",
34
+ "use sup while you edit your message.", "",
35
+ "Press <Enter> to have the file path copied to the clipboard.", "",
36
+ "When you have finished editing, select this buffer and press 'E'.",]
37
+ super()
38
+ end
39
+
40
+ def lines; @text.length end
41
+
42
+ def [] i
43
+ @text[i]
44
+ end
45
+
46
+ def killable?
47
+ if file_being_edited?
48
+ if !BufferManager.ask_yes_or_no("It appears the file is still being edited. Are you sure?")
49
+ return false
50
+ end
51
+ end
52
+
53
+ @parent_edit_mode.edit_message_async_resume true
54
+ true
55
+ end
56
+
57
+ def unsaved?
58
+ !file_being_edited? && !file_has_been_edited?
59
+ end
60
+
61
+ protected
62
+
63
+ def edit_finished
64
+ if file_being_edited?
65
+ if !BufferManager.ask_yes_or_no("It appears the file is still being edited. Are you sure?")
66
+ return false
67
+ end
68
+ end
69
+
70
+ @parent_edit_mode.edit_message_async_resume
71
+ BufferManager.kill_buffer buffer
72
+ true
73
+ end
74
+
75
+ def path_to_clipboard
76
+ if system("which xsel > /dev/null 2>&1")
77
+ # linux/unix path
78
+ IO.popen('xsel --clipboard --input', 'r+') { |clipboard| clipboard.puts(@file_path) }
79
+ BufferManager.flash "Copied file path to clipboard."
80
+ elsif system("which pbcopy > /dev/null 2>&1")
81
+ # mac path
82
+ IO.popen('pbcopy', 'r+') { |clipboard| clipboard.puts(@file_path) }
83
+ BufferManager.flash "Copied file path to clipboard."
84
+ else
85
+ BufferManager.flash "No way to copy text to clipboard - try installing xsel."
86
+ end
87
+ end
88
+
89
+ def run_async_hook
90
+ HookManager.run("async-edit", {:file_path => @file_path})
91
+ end
92
+
93
+ def file_being_edited?
94
+ # check for common editor lock files
95
+ vim_lock_file = File.join(File.dirname(@file_path), '.'+File.basename(@file_path)+'.swp')
96
+ emacs_lock_file = File.join(File.dirname(@file_path), '.#'+File.basename(@file_path))
97
+
98
+ return true if File.exist?(vim_lock_file) || File.exist?(emacs_lock_file)
99
+
100
+ false
101
+ end
102
+
103
+ def file_has_been_edited?
104
+ File.mtime(@file_path) > @orig_mtime
105
+ end
106
+
107
+ end
108
+
109
+ end