PageRenderTime 48ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/app/ui/editor/editor.rb

https://github.com/tilthouse/hacketyhack
Ruby | 456 lines | 388 code | 40 blank | 28 comment | 74 complexity | 0f8c27639548697a0febbc6a742c6c2c MD5 | raw file
  1. # the code editor tab contents
  2. class HH::SideTabs::Editor < HH::SideTab
  3. # common code between InsertionAction and DeletionAction
  4. # on_insert_text and on_delete_text should be called before any subclass
  5. # can be used
  6. class InsertionDeletionCommand
  7. def self.on_insert_text &block
  8. @@insert_text = block
  9. end
  10. def self.on_delete_text &block
  11. @@delete_text = block
  12. end
  13. # action to insert/delete str to text at position pos
  14. def initialize pos, str
  15. @position, @string = pos, str
  16. end
  17. def insert
  18. @@insert_text.call(@position, @string)
  19. end
  20. def delete
  21. @@delete_text.call(@position, @string.size)
  22. end
  23. protected
  24. attr_accessor :position, :string
  25. end
  26. class InsertionCommand < InsertionDeletionCommand
  27. alias execute insert
  28. alias unexecute delete
  29. # returns nil if not mergeble
  30. def merge_with second
  31. if second.class != self.class
  32. nil
  33. elsif second.position != self.position + self.string.size
  34. nil
  35. elsif second.string == "\n"
  36. nil # newlines always start a new command
  37. else
  38. self.string += second.string
  39. self
  40. end
  41. end
  42. end
  43. class DeletionCommand < InsertionDeletionCommand
  44. alias execute delete
  45. alias unexecute insert
  46. def merge_with second
  47. if second.class != self.class
  48. nil
  49. elsif second.string == "\n"
  50. nil
  51. elsif second.position == self.position
  52. # probably the delete key
  53. self.string += second.string
  54. self
  55. elsif self.position == second.position + second.string.size
  56. # probably the backspace key
  57. self.position = second.position
  58. self.string = second.string + self.string
  59. self
  60. else
  61. nil
  62. end
  63. end
  64. end
  65. module UndoRedo
  66. def reset_undo_redo
  67. @command_stack = [] # array of actions
  68. @stack_position = 0;
  69. @last_position = nil
  70. end
  71. # _act was added for consistency with redo_act
  72. def undo_command
  73. return if @stack_position == 0
  74. @stack_position -= 1;
  75. @command_stack[@stack_position].unexecute;
  76. end
  77. # _act was added because redo is a keyword
  78. def redo_command
  79. return if @stack_position == @command_stack.size
  80. @command_stack[@stack_position].execute
  81. @stack_position += 1;
  82. end
  83. def add_command cmd
  84. # all redos get removed
  85. @command_stack[@stack_position..-1] = nil
  86. last = @command_stack.last
  87. if last.nil? or not last.merge_with(cmd)
  88. # create new command
  89. @command_stack[@stack_position] = cmd
  90. @stack_position += 1
  91. end
  92. end
  93. end
  94. end # module HH::Editor
  95. class HH::SideTabs::Editor
  96. include HH::Markup
  97. include UndoRedo
  98. def content
  99. draw_content
  100. end
  101. def load script
  102. if not @save_button.hidden
  103. # current script is unsaved
  104. name = @script[:name] || "An unnamed program"
  105. if not confirm("#{name} has not been saved, if you continue \n" +
  106. " all unsaved modifications will be lost")
  107. return false
  108. end
  109. end
  110. clear {draw_content script}
  111. true
  112. end
  113. # asks confirmation and then saves (or not if save is)
  114. def save_if_confirmed
  115. if not @save_button.hidden
  116. name = @script[:name] || "unnamed program"
  117. question = "I'm going to save modifications to \"#{name}\". Is that okay?\n" +
  118. "Press OK if it is, and cancel if it's not."
  119. if confirm(question)
  120. save @script[:name]
  121. true
  122. else
  123. false
  124. end
  125. end
  126. end
  127. def draw_content(script = {})
  128. @str = script[:script] || ""
  129. name = script[:name] || "A New Program"
  130. @script = script
  131. reset_undo_redo
  132. InsertionDeletionCommand.on_insert_text {|pos, str| insert_text(pos, str)}
  133. InsertionDeletionCommand.on_delete_text {|pos, len| delete_text(pos, len)}
  134. @editor = stack :margin_left => 10, :margin_top => 10, :width => 1.0, :height => 92 do
  135. @sname = subtitle name, :font => "Lacuna Regular", :size => 22,
  136. :margin => 0, :wrap => "trim"
  137. @stale = para(script[:mtime] ? "Last saved #{script[:mtime].since} ago." :
  138. "Not yet saved.", :margin => 0, :stroke => "#39C")
  139. glossb "New Program", :top => 0, :right => 0, :width => 160 do
  140. load({})
  141. end
  142. end
  143. stack :margin_left => 0, :width => 1.0, :height => -92 do
  144. background white(0.4), :width => 38
  145. @scroll =
  146. flow :width => 1.0, :height => 1.0, :margin => 2, :scroll => true do
  147. stack :width => 37, :margin_right => 6 do
  148. @ln = para "1", :font => "Liberation Mono", :size => 10, :stroke => "#777", :align => "right"
  149. end
  150. stack :width => -37, :margin_left => 6, :margin_bottom => 60 do
  151. @t = para "", :font => "Liberation Mono", :size => 10, :stroke => "#662",
  152. :wrap => "trim", :margin_right => 28
  153. @t.cursor = 0
  154. def @t.hit_sloppy(x, y)
  155. x -= 6
  156. c = hit(x, y)
  157. if c
  158. c + 1
  159. elsif x <= 48
  160. hit(48, y)
  161. end
  162. end
  163. end
  164. motion do |x, y|
  165. c = @t.hit_sloppy(x, y)
  166. if c
  167. if self.cursor == :arrow
  168. self.cursor = :text
  169. end
  170. if self.mouse[0] == 1 and @clicked
  171. if @t.marker.nil?
  172. @t.marker = c
  173. else
  174. @t.cursor = c
  175. end
  176. end
  177. elsif self.cursor == :text
  178. self.cursor = :arrow
  179. end
  180. end
  181. release do
  182. @clicked = false
  183. end
  184. click do |_, x, y|
  185. c = @t.hit_sloppy(x, y)
  186. if c
  187. @clicked = true
  188. @t.marker = nil
  189. @t.cursor = c
  190. end
  191. update_text
  192. end
  193. leave { self.cursor = :arrow }
  194. end
  195. end
  196. stack :height => 40, :width => 182, :bottom => -3, :right => 0 do
  197. @copy_button =
  198. glossb "Copy", :width => 60, :top => 2, :left => 70 do
  199. save(nil)
  200. end
  201. @save_button =
  202. glossb "Save", :width => 60, :top => 2, :left => 70, :hidden => true do
  203. if save(script[:name])
  204. timer 0.1 do
  205. @save_button.hide
  206. @copy_button.show
  207. @save_to_cloud_button.show
  208. end
  209. end
  210. end
  211. @save_to_cloud_button =
  212. glossb "Upload", :width => 70, :top => 2, :left => 0 do
  213. if HH::PREFS['username'].nil?
  214. alert("To upload, first connect your account on hackety-hack.com by clicking Preferences near the bottom left of the window.")
  215. else
  216. hacker = Hacker.new :username => HH::PREFS['username'], :password => HH::PREFS['password']
  217. hacker.save_program_to_the_cloud(script[:name].to_slug, @str) do |response|
  218. if response.status == 200
  219. alert("Uploaded!")
  220. else
  221. alert("There was a problem, sorry!")
  222. end
  223. end
  224. end
  225. end
  226. glossb "Run", :width => 52, :top => 2, :left => 130 do
  227. eval(@str, HH.anonymous_binding)
  228. end
  229. end
  230. every 20 do
  231. if script[:mtime]
  232. @stale.text = "Last saved #{script[:mtime].since} ago."
  233. end
  234. end
  235. def onkey(k)
  236. case k when :shift_home, :shift_end, :shift_up, :shift_left, :shift_down, :shift_right
  237. @t.marker = @t.cursor unless @t.marker
  238. when :home, :end, :up, :left, :down, :right
  239. @t.marker = nil
  240. end
  241. case k
  242. when String
  243. if k == "\n"
  244. # handle indentation
  245. ind = indentation_size
  246. handle_text_insertion(k)
  247. handle_text_insertion(" " * ind) if ind > 0
  248. else
  249. # usual case
  250. handle_text_insertion(k)
  251. end
  252. when :backspace, :shift_backspace, :control_backspace
  253. if @t.cursor > 0 and @t.marker.nil?
  254. @t.marker = @t.cursor - 1 # make highlight length at least 1
  255. end
  256. sel = @t.highlight
  257. if sel[0] > 0 or sel[1] > 0
  258. handle_text_deletion(*sel)
  259. end
  260. when :delete
  261. sel = @t.highlight
  262. sel[1] = 1 if sel[1] == 0
  263. handle_text_deletion(*sel)
  264. when :tab
  265. handle_text_insertion(" ")
  266. # when :alt_q
  267. # @action.clear { home }
  268. when :control_a, :alt_a
  269. @t.marker = 0
  270. @t.cursor = @str.length
  271. when :control_x, :alt_x
  272. if @t.marker
  273. sel = @t.highlight
  274. self.clipboard = @str[*sel]
  275. if sel[1] == 0
  276. sel[1] = 1
  277. raise "why did this happen??"
  278. end
  279. handle_text_deletion(*sel)
  280. end
  281. when :control_c, :alt_c, :control_insertadd_characte
  282. if @t.marker
  283. self.clipboard = @str[*@t.highlight]
  284. end
  285. when :control_v, :alt_v, :shift_insert
  286. handle_text_insertion(self.clipboard) if self.clipboard
  287. when :control_z
  288. debug("undo!")
  289. undo_command
  290. when :control_y, :alt_Z, :shift_alt_z
  291. redo_command
  292. when :shift_home, :home
  293. nl = @str.rindex("\n", @t.cursor - 1) || -1
  294. @t.cursor = nl + 1
  295. when :shift_end, :end
  296. nl = @str.index("\n", @t.cursor) || @str.length
  297. @t.cursor = nl
  298. when :shift_up, :up
  299. if @t.cursor > 0
  300. nl = @str.rindex("\n", @t.cursor - 1)
  301. if nl
  302. horz = @t.cursor - nl
  303. upnl = @str.rindex("\n", nl - 1) || -1
  304. @t.cursor = upnl + horz
  305. @t.cursor = nl if @t.cursor > nl
  306. end
  307. end
  308. when :shift_down, :down
  309. nl = @str.index("\n", @t.cursor)
  310. if nl
  311. if @t.cursor > 0
  312. horz = @t.cursor - (@str.rindex("\n", @t.cursor - 1) || -1)
  313. else
  314. horz = 1
  315. end
  316. dnl = @str.index("\n", nl + 1) || @str.length
  317. @t.cursor = nl + horz
  318. @t.cursor = dnl if @t.cursor > dnl
  319. end
  320. when :shift_right, :right
  321. @t.cursor += 1 if @t.cursor < @str.length
  322. when :shift_left, :left
  323. @t.cursor -= 1 if @t.cursor > 0
  324. end
  325. if k
  326. text_changed
  327. end
  328. update_text
  329. end
  330. spaces = [?\t, ?\s, ?\n]
  331. keypress do |k|
  332. onkey(k)
  333. if @t.cursor_top < @scroll.scroll_top
  334. @scroll.scroll_top = @t.cursor_top
  335. elsif @t.cursor_top + 92 > @scroll.scroll_top + @scroll.height
  336. @scroll.scroll_top = (@t.cursor_top + 92) - @scroll.height
  337. end
  338. end
  339. # for samples do not allow to upload to cloud when just opened
  340. @save_to_cloud_button.hide if script[:sample]
  341. update_text
  342. end
  343. # saves the file, asks for a new name if a nil argument is passed
  344. def save name
  345. if name.nil?
  346. msg = ""
  347. while true
  348. name = ask(msg + "Give your program a name.")
  349. break if name.nil? or not HH.script_exists?(name)
  350. msg = "You already have a program named '" + name + "'.\n"
  351. end
  352. end
  353. if name
  354. @script[:name] = name
  355. HH.save_script(@script[:name], @str)
  356. @script[:mtime] = Time.now
  357. @sname.text = @script[:name]
  358. @stale.text = "Last saved #{@script[:mtime].since} ago."
  359. true
  360. else
  361. false
  362. end
  363. end
  364. def update_text
  365. @t.replace *highlight(@str, @t.cursor)
  366. @ln.replace [*1..(@str.count("\n")+1)].join("\n")
  367. end
  368. def text_changed
  369. if @save_button.hidden
  370. @copy_button.hide
  371. @save_button.show
  372. @save_to_cloud_button.hide
  373. end
  374. end
  375. # find the indentation level at the current cursor or marker
  376. # whatever occurs first
  377. # the result is the number of spaces
  378. def indentation_size
  379. # TODO marker
  380. pos = @str.rindex("\n", @t.cursor-1)
  381. return 0 if pos.nil?
  382. pos += 1
  383. ind_size = 0
  384. while @str[pos, 1] == ' '
  385. ind_size += 1
  386. pos += 1
  387. end
  388. ind_size
  389. end
  390. # called when the user wants to insert text
  391. def handle_text_insertion str
  392. pos, len = @t.highlight;
  393. handle_text_deletion(pos, len) if len > 0
  394. add_command InsertionCommand.new(pos, str)
  395. insert_text(pos, str)
  396. end
  397. # called when the user wants to delete text
  398. def handle_text_deletion pos, len
  399. str = @str[pos, len]
  400. return if str.empty? # happens if len == 0 or pos to big
  401. add_command DeletionCommand.new(pos, str)
  402. delete_text(pos, len)
  403. end
  404. def insert_text pos, text
  405. @str.insert(pos, text)
  406. @t.cursor = pos + text.size
  407. @t.cursor = :marker # XXX ???
  408. #update_text
  409. end
  410. def delete_text pos, len
  411. @str[pos, len] = "" # TODO use slice?
  412. @t.cursor = pos
  413. @t.cursor = :marker
  414. #update_text
  415. end
  416. end