'
+ #
+ # # Supplying custom options for the list element
+ # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" }
+ # # => '
Hello
'
+ #
+ # Returns a list item element String
+ def nav_link(options = {}, &block)
+ if path = options.delete(:path)
+ c, a, _ = path.split('#')
+ else
+ c = options.delete(:controller)
+ a = options.delete(:action)
+ end
- def wall_tab?
- current_page?(controller: "projects", action: "wall", id: @project)
+ if c && a
+ # When given both options, make sure BOTH are active
+ klass = current_controller?(*c) && current_action?(*a) ? 'active' : ''
+ else
+ # Otherwise check EITHER option
+ klass = current_controller?(*c) || current_action?(*a) ? 'active' : ''
+ end
+
+ # Add our custom class into the html_options, which may or may not exist
+ # and which may or may not already have a :class key
+ o = options.delete(:html_options) || {}
+ o[:class] ||= ''
+ o[:class] += ' ' + klass
+ o[:class].strip!
+
+ if block_given?
+ content_tag(:li, capture(&block), o)
+ else
+ content_tag(:li, nil, o)
+ end
end
def project_tab_class
[:show, :files, :edit, :update].each do |action|
- return "current" if current_page?(controller: "projects", action: action, id: @project)
+ return "active" if current_page?(controller: "projects", action: action, id: @project)
end
if ['snippets', 'hooks', 'deploy_keys', 'team_members'].include? controller.controller_name
- "current"
- end
- end
-
- def tree_tab_class
- controller.controller_name == "refs" ? "current" : nil
- end
-
- def commit_tab_class
- if ['commits', 'repositories', 'protected_branches'].include? controller.controller_name
- "current"
+ "active"
end
end
def branches_tab_class
if current_page?(branches_project_repository_path(@project)) ||
- controller.controller_name == "protected_branches" ||
+ current_controller?(:protected_branches) ||
current_page?(project_repository_path(@project))
'active'
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 2b7265ca..4fe87a25 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -1,31 +1,42 @@
module TreeHelper
- def tree_icon(content)
- if content.is_a?(Grit::Blob)
- if content.text?
- image_tag "file_txt.png"
- elsif content.image?
- image_tag "file_img.png"
+ # Sorts a repository's tree so that folders are before files and renders
+ # their corresponding partials
+ #
+ # contents - A Grit::Tree object for the current tree
+ def render_tree(contents)
+ # Render Folders before Files/Submodules
+ folders, files = contents.partition { |v| v.kind_of?(Grit::Tree) }
+
+ tree = ""
+
+ # Render folders if we have any
+ tree += render partial: 'tree/tree_item', collection: folders, locals: {type: 'folder'} if folders.present?
+
+ files.each do |f|
+ if f.respond_to?(:url)
+ # Object is a Submodule
+ tree += render partial: 'tree/submodule_item', object: f
else
- image_tag "file_bin.png"
+ # Object is a Blob
+ tree += render partial: 'tree/tree_item', object: f, locals: {type: 'file'}
end
- else
- image_tag "file_dir.png"
end
+
+ tree.html_safe
+ end
+
+ # Return an image icon depending on the file type
+ #
+ # type - String type of the tree item; either 'folder' or 'file'
+ def tree_icon(type)
+ image = type == 'folder' ? 'file_dir.png' : 'file_txt.png'
+ image_tag(image, size: '16x16')
end
def tree_hex_class(content)
"file_#{hexdigest(content.name)}"
end
- def tree_full_path(content)
- content.name.force_encoding('utf-8')
- if params[:path]
- File.join(params[:path], content.name)
- else
- content.name
- end
- end
-
# Public: Determines if a given filename is compatible with GitHub::Markup.
#
# filename - Filename string to check
@@ -39,4 +50,21 @@ module TreeHelper
def gitlab_markdown?(filename)
filename.end_with?(*%w(.mdown .md .markdown))
end
+
+ def plain_text_readme? filename
+ filename == 'README'
+ end
+
+ # Simple shortcut to File.join
+ def tree_join(*args)
+ File.join(*args)
+ end
+
+ def allowed_tree_edit?
+ if @project.protected_branch? @ref
+ can?(current_user, :push_code_to_protected_branches, @project)
+ else
+ can?(current_user, :push_code, @project)
+ end
+ end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0afc1d31..7f3862ad 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -9,11 +9,11 @@ class Notify < ActionMailer::Base
default from: Gitlab.config.email_from
- def new_user_email(user_id, password)
- @user = User.find(user_id)
- @password = password
- mail(to: @user.email, subject: subject("Account was created for you"))
- end
+
+
+ #
+ # Issue
+ #
def new_issue_email(issue_id)
@issue = Issue.find(issue_id)
@@ -21,40 +21,26 @@ class Notify < ActionMailer::Base
mail(to: @issue.assignee_email, subject: subject("new issue ##{@issue.id}", @issue.title))
end
- def note_wall_email(recipient_id, note_id)
- @note = Note.find(note_id)
- @project = @note.project
- mail(to: recipient(recipient_id), subject: subject)
+ def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id)
+ @issue = Issue.find(issue_id)
+ @previous_assignee ||= User.find(previous_assignee_id)
+ @project = @issue.project
+ mail(to: recipient(recipient_id), subject: subject("changed issue ##{@issue.id}", @issue.title))
end
- def note_commit_email(recipient_id, note_id)
- @note = Note.find(note_id)
- @commit = @note.target
- @commit = CommitDecorator.decorate(@commit)
- @project = @note.project
- mail(to: recipient(recipient_id), subject: subject("note for commit #{@commit.short_id}", @commit.title))
+ def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
+ @issue = Issue.find issue_id
+ @issue_status = status
+ @updated_by = User.find updated_by_user_id
+ mail(to: recipient(recipient_id),
+ subject: subject("changed issue ##{@issue.id}", @issue.title))
end
- def note_merge_request_email(recipient_id, note_id)
- @note = Note.find(note_id)
- @merge_request = @note.noteable
- @project = @note.project
- mail(to: recipient(recipient_id), subject: subject("note for merge request !#{@merge_request.id}"))
- end
- def note_issue_email(recipient_id, note_id)
- @note = Note.find(note_id)
- @issue = @note.noteable
- @project = @note.project
- mail(to: recipient(recipient_id), subject: subject("note for issue ##{@issue.id}"))
- end
- def note_wiki_email(recipient_id, note_id)
- @note = Note.find(note_id)
- @wiki = @note.noteable
- @project = @note.project
- mail(to: recipient(recipient_id), subject: subject("note for wiki"))
- end
+ #
+ # Merge Request
+ #
def new_merge_request_email(merge_request_id)
@merge_request = MergeRequest.find(merge_request_id)
@@ -69,13 +55,53 @@ class Notify < ActionMailer::Base
mail(to: recipient(recipient_id), subject: subject("changed merge request !#{@merge_request.id}", @merge_request.title))
end
- def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id)
- @issue = Issue.find(issue_id)
- @previous_assignee ||= User.find(previous_assignee_id)
- @project = @issue.project
- mail(to: recipient(recipient_id), subject: subject("changed issue ##{@issue.id}", @issue.title))
+
+
+ #
+ # Note
+ #
+
+ def note_commit_email(recipient_id, note_id)
+ @note = Note.find(note_id)
+ @commit = @note.noteable
+ @commit = CommitDecorator.decorate(@commit)
+ @project = @note.project
+ mail(to: recipient(recipient_id), subject: subject("note for commit #{@commit.short_id}", @commit.title))
end
+ def note_issue_email(recipient_id, note_id)
+ @note = Note.find(note_id)
+ @issue = @note.noteable
+ @project = @note.project
+ mail(to: recipient(recipient_id), subject: subject("note for issue ##{@issue.id}"))
+ end
+
+ def note_merge_request_email(recipient_id, note_id)
+ @note = Note.find(note_id)
+ @merge_request = @note.noteable
+ @project = @note.project
+ mail(to: recipient(recipient_id), subject: subject("note for merge request !#{@merge_request.id}"))
+ end
+
+ def note_wall_email(recipient_id, note_id)
+ @note = Note.find(note_id)
+ @project = @note.project
+ mail(to: recipient(recipient_id), subject: subject)
+ end
+
+ def note_wiki_email(recipient_id, note_id)
+ @note = Note.find(note_id)
+ @wiki = @note.noteable
+ @project = @note.project
+ mail(to: recipient(recipient_id), subject: subject("note for wiki"))
+ end
+
+
+
+ #
+ # Project
+ #
+
def project_access_granted_email(user_project_id)
@users_project = UsersProject.find user_project_id
@project = @users_project.project
@@ -83,14 +109,19 @@ class Notify < ActionMailer::Base
subject: subject("access to project was granted"))
end
- def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
- @issue = Issue.find issue_id
- @issue_status = status
- @updated_by = User.find updated_by_user_id
- mail(to: recipient(recipient_id),
- subject: subject("changed issue ##{@issue.id}", @issue.title))
+
+
+ #
+ # User
+ #
+
+ def new_user_email(user_id, password)
+ @user = User.find(user_id)
+ @password = password
+ mail(to: @user.email, subject: subject("Account was created for you"))
end
+
private
# Look up a User by their ID and return their email address
diff --git a/app/models/ability.rb b/app/models/ability.rb
index d65695a2..c3a212f4 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,62 +1,66 @@
class Ability
- def self.allowed(object, subject)
- case subject.class.name
- when "Project" then project_abilities(object, subject)
- when "Issue" then issue_abilities(object, subject)
- when "Note" then note_abilities(object, subject)
- when "Snippet" then snippet_abilities(object, subject)
- when "MergeRequest" then merge_request_abilities(object, subject)
- else []
- end
- end
-
- def self.project_abilities(user, project)
- rules = []
-
- rules << [
- :read_project,
- :read_wiki,
- :read_issue,
- :read_milestone,
- :read_snippet,
- :read_team_member,
- :read_merge_request,
- :read_note,
- :write_project,
- :write_issue,
- :write_note
- ] if project.guest_access_for?(user)
-
- rules << [
- :download_code,
- :write_merge_request,
- :write_snippet
- ] if project.report_access_for?(user)
-
- rules << [
- :write_wiki
- ] if project.dev_access_for?(user)
-
- rules << [
- :modify_issue,
- :modify_snippet,
- :modify_merge_request,
- :admin_project,
- :admin_issue,
- :admin_milestone,
- :admin_snippet,
- :admin_team_member,
- :admin_merge_request,
- :admin_note,
- :accept_mr,
- :admin_wiki
- ] if project.master_access_for?(user) || project.owner == user
-
-
- rules.flatten
- end
-
class << self
+ def allowed(object, subject)
+ case subject.class.name
+ when "Project" then project_abilities(object, subject)
+ when "Issue" then issue_abilities(object, subject)
+ when "Note" then note_abilities(object, subject)
+ when "Snippet" then snippet_abilities(object, subject)
+ when "MergeRequest" then merge_request_abilities(object, subject)
+ else []
+ end
+ end
+
+ def project_abilities(user, project)
+ rules = []
+
+ rules << [
+ :read_project,
+ :read_wiki,
+ :read_issue,
+ :read_milestone,
+ :read_snippet,
+ :read_team_member,
+ :read_merge_request,
+ :read_note,
+ :write_project,
+ :write_issue,
+ :write_note
+ ] if project.guest_access_for?(user)
+
+ rules << [
+ :download_code,
+ :write_merge_request,
+ :write_snippet
+ ] if project.report_access_for?(user)
+
+ rules << [
+ :write_wiki,
+ :push_code
+ ] if project.dev_access_for?(user)
+
+ rules << [
+ :push_code_to_protected_branches
+ ] if project.master_access_for?(user)
+
+ rules << [
+ :modify_issue,
+ :modify_snippet,
+ :modify_merge_request,
+ :admin_project,
+ :admin_issue,
+ :admin_milestone,
+ :admin_snippet,
+ :admin_team_member,
+ :admin_merge_request,
+ :admin_note,
+ :accept_mr,
+ :admin_wiki
+ ] if project.master_access_for?(user) || project.owner == user
+
+ rules.flatten
+ end
+
[:issue, :note, :snippet, :merge_request].each do |name|
define_method "#{name}_abilities" do |user, subject|
if subject.author == user
@@ -73,8 +77,7 @@ class Ability
:"modify_#{name}",
]
else
- subject.respond_to?(:project) ?
- project_abilities(user, subject.project) : []
+ subject.respond_to?(:project) ? project_abilities(user, subject.project) : []
end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 73583e9e..a070e830 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -4,24 +4,11 @@ class Commit
include StaticModel
extend ActiveModel::Naming
- attr_accessor :commit
- attr_accessor :head
- attr_accessor :refs
+ attr_accessor :commit, :head, :refs
- delegate :message,
- :authored_date,
- :committed_date,
- :parents,
- :sha,
- :date,
- :committer,
- :author,
- :message,
- :diffs,
- :tree,
- :id,
- :to_patch,
- to: :commit
+ delegate :message, :authored_date, :committed_date, :parents, :sha,
+ :date, :committer, :author, :message, :diffs, :tree, :id,
+ :to_patch, to: :commit
class << self
def find_or_first(repo, commit_id = nil, root_ref)
@@ -30,6 +17,7 @@ class Commit
else
repo.commits(root_ref).first
end
+
Commit.new(commit) if commit
end
@@ -119,7 +107,7 @@ class Commit
end
def safe_message
- utf8 message
+ @safe_message ||= utf8 message
end
def created_at
diff --git a/app/models/event.rb b/app/models/event.rb
index b11b21bd..0ea3224a 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -1,6 +1,9 @@
class Event < ActiveRecord::Base
include PushEvent
+ attr_accessible :project, :action, :data, :author_id, :project_id,
+ :target_id, :target_type
+
default_scope where("author_id IS NOT NULL")
Created = 1
@@ -13,27 +16,32 @@ class Event < ActiveRecord::Base
Joined = 8 # User joined project
Left = 9 # User left project
+ delegate :name, :email, to: :author, prefix: true, allow_nil: true
+ delegate :title, to: :issue, prefix: true, allow_nil: true
+ delegate :title, to: :merge_request, prefix: true, allow_nil: true
+
+ belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :target, polymorphic: true
# For Hash only
serialize :data
+ # Scopes
scope :recent, order("created_at DESC")
scope :code_push, where(action: Pushed)
+ scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent }
- def self.determine_action(record)
- if [Issue, MergeRequest].include? record.class
- Event::Created
- elsif record.kind_of? Note
- Event::Commented
+ class << self
+ def determine_action(record)
+ if [Issue, MergeRequest].include? record.class
+ Event::Created
+ elsif record.kind_of? Note
+ Event::Commented
+ end
end
end
- def self.recent_for_user user
- where(project_id: user.projects.map(&:id)).recent
- end
-
# Next events currently enabled for system
# - push
# - new issue
@@ -46,10 +54,14 @@ class Event < ActiveRecord::Base
if project
project.name
else
- "(deleted)"
+ "(deleted project)"
end
end
+ def target_title
+ target.try :title
+ end
+
def push?
action == self.class::Pushed && valid_push?
end
@@ -131,24 +143,21 @@ class Event < ActiveRecord::Base
"opened"
end
end
-
- delegate :name, :email, to: :author, prefix: true, allow_nil: true
- delegate :title, to: :issue, prefix: true, allow_nil: true
- delegate :title, to: :merge_request, prefix: true, allow_nil: true
end
+
# == Schema Information
#
# Table name: events
#
-# id :integer(4) not null, primary key
+# id :integer not null, primary key
# target_type :string(255)
-# target_id :integer(4)
+# target_id :integer
# title :string(255)
# data :text
-# project_id :integer(4)
+# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
-# action :integer(4)
-# author_id :integer(4)
+# action :integer
+# author_id :integer
#
diff --git a/app/models/group.rb b/app/models/group.rb
new file mode 100644
index 00000000..ef8c7463
--- /dev/null
+++ b/app/models/group.rb
@@ -0,0 +1,37 @@
+class Group < ActiveRecord::Base
+ attr_accessible :code, :name, :owner_id
+
+ has_many :projects
+ belongs_to :owner, class_name: "User"
+
+ validates :name, presence: true, uniqueness: true
+ validates :code, presence: true, uniqueness: true
+ validates :owner, presence: true
+
+ delegate :name, to: :owner, allow_nil: true, prefix: true
+
+ def self.search query
+ where("name LIKE :query OR code LIKE :query", query: "%#{query}%")
+ end
+
+ def to_param
+ code
+ end
+
+ def users
+ User.joins(:users_projects).where(users_projects: {project_id: project_ids}).uniq
+ end
+end
+
+# == Schema Information
+#
+# Table name: groups
+#
+# id :integer not null, primary key
+# name :string(255) not null
+# code :string(255) not null
+# owner_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 96a54907..3dd1c8c8 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -2,49 +2,35 @@ class Issue < ActiveRecord::Base
include IssueCommonality
include Votes
+ attr_accessible :title, :assignee_id, :closed, :position, :description,
+ :milestone_id, :label_list, :author_id_of_changes
+
acts_as_taggable_on :labels
belongs_to :milestone
- validates :description,
- length: { within: 0..2000 }
+ validates :description, length: { within: 0..2000 }
def self.open_for(user)
opened.assigned(user)
end
-
- def is_assigned?
- !!assignee_id
- end
-
- def is_being_reassigned?
- assignee_id_changed?
- end
-
- def is_being_closed?
- closed_changed? && closed
- end
-
- def is_being_reopened?
- closed_changed? && !closed
- end
end
+
# == Schema Information
#
# Table name: issues
#
-# id :integer(4) not null, primary key
+# id :integer not null, primary key
# title :string(255)
-# assignee_id :integer(4)
-# author_id :integer(4)
-# project_id :integer(4)
+# assignee_id :integer
+# author_id :integer
+# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
-# closed :boolean(1) default(FALSE), not null
-# position :integer(4) default(0)
-# critical :boolean(1) default(FALSE), not null
+# closed :boolean default(FALSE), not null
+# position :integer default(0)
# branch_name :string(255)
# description :text
-# milestone_id :integer(4)
+# milestone_id :integer
#
diff --git a/app/models/key.rb b/app/models/key.rb
index a39a4a16..e4710b85 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -4,21 +4,16 @@ class Key < ActiveRecord::Base
belongs_to :user
belongs_to :project
- attr_protected :user_id
+ attr_accessible :key, :title
- validates :title,
- presence: true,
- length: { within: 0..255 }
-
- validates :key,
- presence: true,
- format: { :with => /ssh-.{3} / },
- length: { within: 0..5000 }
-
- before_save :set_identifier
before_validation :strip_white_space
+ before_save :set_identifier
+
+ validates :title, presence: true, length: { within: 0..255 }
+ validates :key, presence: true, length: { within: 0..5000 }, format: { :with => /ssh-.{3} / }
+ validate :unique_key, :fingerprintable_key
+
delegate :name, :email, to: :user, prefix: true
- validate :unique_key
def strip_white_space
self.key = self.key.strip unless self.key.blank?
@@ -32,9 +27,24 @@ class Key < ActiveRecord::Base
end
end
+ def fingerprintable_key
+ return true unless key # Don't test if there is no key.
+ # `ssh-keygen -lf /dev/stdin <<< "#{key}"` errors with: redirection unexpected
+ file = Tempfile.new('key_file')
+ begin
+ file.puts key
+ file.rewind
+ fingerprint_output = `ssh-keygen -lf #{file.path} 2>&1` # Catch stderr.
+ ensure
+ file.close
+ file.unlink # deletes the temp file
+ end
+ errors.add(:key, "can't be fingerprinted") if fingerprint_output.match("failed")
+ end
+
def set_identifier
if is_deploy_key
- self.identifier = "deploy_" + Digest::MD5.hexdigest(key)
+ self.identifier = "deploy_#{Digest::MD5.hexdigest(key)}"
else
self.identifier = "#{user.identifier}_#{Time.now.to_i}"
end
@@ -57,17 +67,18 @@ class Key < ActiveRecord::Base
Key.where(identifier: identifier).count == 0
end
end
+
# == Schema Information
#
# Table name: keys
#
-# id :integer(4) not null, primary key
-# user_id :integer(4)
+# id :integer not null, primary key
+# user_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# key :text
# title :string(255)
# identifier :string(255)
-# project_id :integer(4)
+# project_id :integer
#
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 184ac5fc..70780b75 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1,9 +1,14 @@
-require File.join(Rails.root, "app/models/commit")
+require Rails.root.join("app/models/commit")
class MergeRequest < ActiveRecord::Base
include IssueCommonality
include Votes
+ attr_accessible :title, :assignee_id, :closed, :target_branch, :source_branch,
+ :author_id_of_changes
+
+ attr_accessor :should_remove_source_branch
+
BROKEN_DIFF = "--broken-diff"
UNCHECKED = 1
@@ -13,14 +18,12 @@ class MergeRequest < ActiveRecord::Base
serialize :st_commits
serialize :st_diffs
- attr_accessor :should_remove_source_branch
-
- validates_presence_of :source_branch
- validates_presence_of :target_branch
+ validates :source_branch, presence: true
+ validates :target_branch, presence: true
validate :validate_branches
def self.find_all_by_branch(branch_name)
- where("source_branch like :branch or target_branch like :branch", branch: branch_name)
+ where("source_branch LIKE :branch OR target_branch LIKE :branch", branch: branch_name)
end
def human_state
@@ -48,7 +51,8 @@ class MergeRequest < ActiveRecord::Base
end
def mark_as_unchecked
- self.update_attributes(state: UNCHECKED)
+ self.state = UNCHECKED
+ self.save
end
def can_be_merged?
@@ -131,7 +135,8 @@ class MergeRequest < ActiveRecord::Base
end
def mark_as_unmergable
- self.update_attributes state: CANNOT_BE_MERGED
+ self.state = CANNOT_BE_MERGED
+ self.save
end
def reloaded_commits
@@ -162,7 +167,7 @@ class MergeRequest < ActiveRecord::Base
end
def automerge!(current_user)
- if Gitlab::Merge.new(self, current_user).merge && self.unmerged_commits.empty?
+ if Gitlab::Merge.new(self, current_user).merge! && self.unmerged_commits.empty?
self.merge!(current_user.id)
true
end
@@ -182,24 +187,30 @@ class MergeRequest < ActiveRecord::Base
patch_path
end
+
+ def mr_and_commit_notes
+ commit_ids = commits.map(&:id)
+ Note.where("(noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR (noteable_type = 'Commit' AND noteable_id IN (:commit_ids))", mr_id: id, commit_ids: commit_ids)
+ end
end
+
# == Schema Information
#
# Table name: merge_requests
#
-# id :integer(4) not null, primary key
+# id :integer not null, primary key
# target_branch :string(255) not null
# source_branch :string(255) not null
-# project_id :integer(4) not null
-# author_id :integer(4)
-# assignee_id :integer(4)
+# project_id :integer not null
+# author_id :integer
+# assignee_id :integer
# title :string(255)
-# closed :boolean(1) default(FALSE), not null
+# closed :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
-# st_commits :text(2147483647
-# st_diffs :text(2147483647
-# merged :boolean(1) default(FALSE), not null
-# state :integer(4) default(1), not null
+# st_commits :text(4294967295
+# st_diffs :text(4294967295
+# merged :boolean default(FALSE), not null
+# state :integer default(1), not null
#
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d416fb63..06c09431 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -1,30 +1,18 @@
-# == Schema Information
-#
-# Table name: milestones
-#
-# id :integer(4) not null, primary key
-# title :string(255) not null
-# project_id :integer(4) not null
-# description :text
-# due_date :date
-# closed :boolean(1) default(FALSE), not null
-# created_at :datetime not null
-# updated_at :datetime not null
-#
-
class Milestone < ActiveRecord::Base
+ attr_accessible :title, :description, :due_date, :closed
+
belongs_to :project
has_many :issues
- validates_presence_of :project_id
- validates_presence_of :title
+ validates :title, presence: true
+ validates :project, presence: true
def self.active
where("due_date > ? OR due_date IS NULL", Date.today)
end
def participants
- User.where(id: issues.map(&:assignee_id))
+ User.where(id: issues.pluck(:assignee_id))
end
def percent_complete
@@ -37,3 +25,18 @@ class Milestone < ActiveRecord::Base
"expires at #{due_date.stamp("Aug 21, 2011")}" if due_date
end
end
+
+# == Schema Information
+#
+# Table name: milestones
+#
+# id :integer not null, primary key
+# title :string(255) not null
+# project_id :integer not null
+# description :text
+# due_date :date
+# closed :boolean default(FALSE), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
diff --git a/app/models/note.rb b/app/models/note.rb
index 34edb94e..e2f4a89d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -2,52 +2,42 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class Note < ActiveRecord::Base
- belongs_to :project
- belongs_to :noteable, polymorphic: true
- belongs_to :author,
- class_name: "User"
- delegate :name,
- to: :project,
- prefix: true
+ attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id,
+ :attachment, :line_code
- delegate :name,
- :email,
- to: :author,
- prefix: true
-
- attr_protected :author, :author_id
attr_accessor :notify
attr_accessor :notify_author
- validates_presence_of :project
+ belongs_to :project
+ belongs_to :noteable, polymorphic: true
+ belongs_to :author, class_name: "User"
- validates :note,
- presence: true,
- length: { within: 0..5000 }
+ delegate :name, to: :project, prefix: true
+ delegate :name, :email, to: :author, prefix: true
- validates :attachment,
- file_size: {
- maximum: 10.megabytes.to_i
- }
+ validates :project, presence: true
+ validates :note, presence: true, length: { within: 0..5000 }
+ validates :attachment, file_size: { maximum: 10.megabytes.to_i }
+ mount_uploader :attachment, AttachmentUploader
+
+ # Scopes
scope :common, where(noteable_id: nil)
-
scope :today, where("created_at >= :date", date: Date.today)
scope :last_week, where("created_at >= :date", date: (Date.today - 7.days))
- scope :since, lambda { |day| where("created_at >= :date", date: (day)) }
+ scope :since, ->(day) { where("created_at >= :date", date: (day)) }
scope :fresh, order("created_at ASC, id ASC")
scope :inc_author_project, includes(:project, :author)
scope :inc_author, includes(:author)
- mount_uploader :attachment, AttachmentUploader
-
def self.create_status_change_note(noteable, author, status)
- create({ noteable: noteable,
- project: noteable.project,
- author: author,
- note: "_Status changed to #{status}_" },
- without_protection: true)
+ create({
+ noteable: noteable,
+ project: noteable.project,
+ author: author,
+ note: "_Status changed to #{status}_"
+ }, without_protection: true)
end
def notify
@@ -58,11 +48,12 @@ class Note < ActiveRecord::Base
@notify_author ||= false
end
- def target
- if noteable_type == "Commit"
+ # override to return commits, which are not active record
+ def noteable
+ if for_commit?
project.commit(noteable_id)
else
- noteable
+ super
end
# Temp fix to prevent app crash
# if note commit id doesnt exist
@@ -84,18 +75,22 @@ class Note < ActiveRecord::Base
# Boolean
#
def notify_only_author?(user)
- commit? && commit_author &&
+ for_commit? && commit_author &&
commit_author.email != user.email
end
- def commit?
+ def for_commit?
noteable_type == "Commit"
end
+ def for_diff_line?
+ line_code.present?
+ end
+
def commit_author
@commit_author ||=
- project.users.find_by_email(target.author_email) ||
- project.users.find_by_name(target.author_name)
+ project.users.find_by_email(noteable.author_email) ||
+ project.users.find_by_name(noteable.author_name)
rescue
nil
end
@@ -112,18 +107,19 @@ class Note < ActiveRecord::Base
note.start_with?('-1') || note.start_with?(':-1:')
end
end
+
# == Schema Information
#
# Table name: notes
#
-# id :integer(4) not null, primary key
+# id :integer not null, primary key
# note :text
# noteable_id :string(255)
# noteable_type :string(255)
-# author_id :integer(4)
+# author_id :integer
# created_at :datetime not null
# updated_at :datetime not null
-# project_id :integer(4)
+# project_id :integer
# attachment :string(255)
# line_code :string(255)
#
diff --git a/app/models/project.rb b/app/models/project.rb
index 56d5d791..53fe0ee1 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -6,9 +6,12 @@ class Project < ActiveRecord::Base
include Authority
include Team
- #
+ attr_accessible :name, :path, :description, :code, :default_branch, :issues_enabled,
+ :wall_enabled, :merge_requests_enabled, :wiki_enabled
+ attr_accessor :error_code
+
# Relations
- #
+ belongs_to :group
belongs_to :owner, class_name: "User"
has_many :users, through: :users_projects
has_many :events, dependent: :destroy
@@ -22,52 +25,66 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: "ProjectHook"
has_many :wikis, dependent: :destroy
has_many :protected_branches, dependent: :destroy
+ has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id'
- attr_accessor :error_code
+ delegate :name, to: :owner, allow_nil: true, prefix: true
- #
- # Protected attributes
- #
- attr_protected :private_flag, :owner_id
+ # Validations
+ validates :owner, presence: true
+ validates :description, length: { within: 0..2000 }
+ validates :name, uniqueness: true, presence: true, length: { within: 0..255 }
+ validates :path, uniqueness: true, presence: true, length: { within: 0..255 },
+ format: { with: /\A[a-zA-Z][a-zA-Z0-9_\-\.]*\z/,
+ message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" }
+ validates :code, presence: true, uniqueness: true, length: { within: 1..255 },
+ format: { with: /\A[a-zA-Z][a-zA-Z0-9_\-\.]*\z/,
+ message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" }
+ validates :issues_enabled, :wall_enabled, :merge_requests_enabled,
+ :wiki_enabled, inclusion: { in: [true, false] }
+ validate :check_limit, :repo_name
- #
# Scopes
- #
scope :public_only, where(private_flag: false)
- scope :without_user, lambda { |user| where("id not in (:ids)", ids: user.projects.map(&:id) ) }
+ scope :without_user, ->(user) { where("id NOT IN (:ids)", ids: user.projects.map(&:id) ) }
+ scope :not_in_group, ->(group) { where("id NOT IN (:ids)", ids: group.project_ids ) }
- def self.active
- joins(:issues, :notes, :merge_requests).order("issues.created_at, notes.created_at, merge_requests.created_at DESC")
- end
-
- def self.search query
- where("name like :query or code like :query or path like :query", query: "%#{query}%")
- end
-
- def self.create_by_user(params, user)
- project = Project.new params
-
- Project.transaction do
- project.owner = user
-
- project.save!
-
- # Add user as project master
- project.users_projects.create!(project_access: UsersProject::MASTER, user: user)
-
- # when project saved no team member exist so
- # project repository should be updated after first user add
- project.update_repository
+ class << self
+ def active
+ joins(:issues, :notes, :merge_requests).order("issues.created_at, notes.created_at, merge_requests.created_at DESC")
end
- project
- rescue Gitlab::Gitolite::AccessDenied => ex
- project.error_code = :gitolite
- project
- rescue => ex
- project.error_code = :db
- project.errors.add(:base, "Can't save project. Please try again later")
- project
+ def search query
+ where("name LIKE :query OR code LIKE :query OR path LIKE :query", query: "%#{query}%")
+ end
+
+ def create_by_user(params, user)
+ project = Project.new params
+
+ Project.transaction do
+ project.owner = user
+ project.save!
+
+ # Add user as project master
+ project.users_projects.create!(project_access: UsersProject::MASTER, user: user)
+
+ # when project saved no team member exist so
+ # project repository should be updated after first user add
+ project.update_repository
+ end
+
+ project
+ rescue Gitlab::Gitolite::AccessDenied => ex
+ project.error_code = :gitolite
+ project
+ rescue => ex
+ project.error_code = :db
+ project.errors.add(:base, "Can't save project. Please try again later")
+ project
+ end
+
+ def access_options
+ UsersProject.access_roles
+ end
end
def git_error?
@@ -78,37 +95,6 @@ class Project < ActiveRecord::Base
id && valid?
end
- #
- # Validations
- #
- validates :name,
- uniqueness: true,
- presence: true,
- length: { within: 0..255 }
-
- validates :path,
- uniqueness: true,
- presence: true,
- format: { with: /^[a-zA-Z][a-zA-Z0-9_\-\.]*$/,
- message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" },
- length: { within: 0..255 }
-
- validates :description,
- length: { within: 0..2000 }
-
- validates :code,
- presence: true,
- uniqueness: true,
- format: { with: /^[a-zA-Z][a-zA-Z0-9_\-\.]*$/,
- message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" },
- length: { within: 1..255 }
-
- validates :owner, presence: true
- validates :issues_enabled, :wall_enabled, :merge_requests_enabled,
- :wiki_enabled, inclusion: { in: [true, false] }
- validate :check_limit
- validate :repo_name
-
def check_limit
unless owner.can_create_project?
errors[:base] << ("Your own projects limit is #{owner.projects_limit}! Please contact administrator to increase it")
@@ -123,10 +109,6 @@ class Project < ActiveRecord::Base
end
end
- def self.access_options
- UsersProject.access_roles
- end
-
def to_param
code
end
@@ -148,7 +130,7 @@ class Project < ActiveRecord::Base
end
def commit_line_notes(commit)
- notes.where(noteable_id: commit.id, noteable_type: "Commit").where("line_code is not null")
+ notes.where(noteable_id: commit.id, noteable_type: "Commit").where("line_code IS NOT NULL")
end
def public?
@@ -160,43 +142,44 @@ class Project < ActiveRecord::Base
end
def last_activity
- events.order("created_at ASC").last
+ last_event
end
def last_activity_date
- if events.last
- events.last.created_at
- else
- updated_at
- end
+ last_event.try(:created_at) || updated_at
end
def wiki_notes
- Note.where(noteable_id: wikis.map(&:id), noteable_type: 'Wiki', project_id: self.id)
+ Note.where(noteable_id: wikis.pluck(:id), noteable_type: 'Wiki', project_id: self.id)
end
def project_id
self.id
end
+
+ def issues_labels
+ issues.tag_counts_on(:labels)
+ end
end
# == Schema Information
#
# Table name: projects
#
-# id :integer(4) not null, primary key
+# id :integer not null, primary key
# name :string(255)
# path :string(255)
# description :text
# created_at :datetime not null
# updated_at :datetime not null
-# private_flag :boolean(1) default(TRUE), not null
+# private_flag :boolean default(TRUE), not null
# code :string(255)
-# owner_id :integer(4)
+# owner_id :integer
# default_branch :string(255)
-# issues_enabled :boolean(1) default(TRUE), not null
-# wall_enabled :boolean(1) default(TRUE), not null
-# merge_requests_enabled :boolean(1) default(TRUE), not null
-# wiki_enabled :boolean(1) default(TRUE), not null
+# issues_enabled :boolean default(TRUE), not null
+# wall_enabled :boolean default(TRUE), not null
+# merge_requests_enabled :boolean default(TRUE), not null
+# wiki_enabled :boolean default(TRUE), not null
+# group_id :integer
#
diff --git a/app/models/project_hook.rb b/app/models/project_hook.rb
index 06388aae..92f6d1f0 100644
--- a/app/models/project_hook.rb
+++ b/app/models/project_hook.rb
@@ -1,3 +1,16 @@
class ProjectHook < WebHook
belongs_to :project
end
+
+# == Schema Information
+#
+# Table name: web_hooks
+#
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# type :string(255) default("ProjectHook")
+#
+
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 7c30f7a0..926692f1 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -1,9 +1,11 @@
class ProtectedBranch < ActiveRecord::Base
include GitHost
+ attr_accessible :name
+
belongs_to :project
- validates_presence_of :project_id
- validates_presence_of :name
+ validates :name, presence: true
+ validates :project, presence: true
after_save :update_repository
after_destroy :update_repository
@@ -16,12 +18,13 @@ class ProtectedBranch < ActiveRecord::Base
project.commit(self.name)
end
end
+
# == Schema Information
#
# Table name: protected_branches
#
-# id :integer(4) not null, primary key
-# project_id :integer(4) not null
+# id :integer not null, primary key
+# project_id :integer not null
# name :string(255) not null
# created_at :datetime not null
# updated_at :datetime not null
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 2c941499..3525219e 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -1,31 +1,21 @@
class Snippet < ActiveRecord::Base
include Linguist::BlobHelper
+ attr_accessible :title, :content, :file_name, :expires_at
+
belongs_to :project
belongs_to :author, class_name: "User"
has_many :notes, as: :noteable, dependent: :destroy
- delegate :name,
- :email,
- to: :author,
- prefix: true
- attr_protected :author, :author_id, :project, :project_id
+ delegate :name, :email, to: :author, prefix: true
- validates_presence_of :project_id
- validates_presence_of :author_id
-
- validates :title,
- presence: true,
- length: { within: 0..255 }
-
- validates :file_name,
- presence: true,
- length: { within: 0..255 }
-
- validates :content,
- presence: true,
- length: { within: 0..10000 }
+ validates :author, presence: true
+ validates :project, presence: true
+ validates :title, presence: true, length: { within: 0..255 }
+ validates :file_name, presence: true, length: { within: 0..255 }
+ validates :content, presence: true, length: { within: 0..10000 }
+ # Scopes
scope :fresh, order("created_at DESC")
scope :non_expired, where(["expires_at IS NULL OR expires_at > ?", Time.current])
scope :expired, where(["expires_at IS NOT NULL AND expires_at < ?", Time.current])
@@ -46,11 +36,11 @@ class Snippet < ActiveRecord::Base
0
end
- def name
+ def name
file_name
end
- def mode
+ def mode
nil
end
@@ -58,15 +48,16 @@ class Snippet < ActiveRecord::Base
expires_at && expires_at < Time.current
end
end
+
# == Schema Information
#
# Table name: snippets
#
-# id :integer(4) not null, primary key
+# id :integer not null, primary key
# title :string(255)
# content :text
-# author_id :integer(4) not null
-# project_id :integer(4) not null
+# author_id :integer not null
+# project_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
# file_name :string(255)
diff --git a/app/models/system_hook.rb b/app/models/system_hook.rb
index 8517d43a..f56b80f4 100644
--- a/app/models/system_hook.rb
+++ b/app/models/system_hook.rb
@@ -1,13 +1,24 @@
class SystemHook < WebHook
-
- def async_execute(data)
- Resque.enqueue(SystemHookWorker, id, data)
- end
-
def self.all_hooks_fire(data)
SystemHook.all.each do |sh|
sh.async_execute data
end
end
-
+
+ def async_execute(data)
+ Resque.enqueue(SystemHookWorker, id, data)
+ end
end
+
+# == Schema Information
+#
+# Table name: web_hooks
+#
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# type :string(255) default("ProjectHook")
+#
+
diff --git a/app/models/tree.rb b/app/models/tree.rb
index d65e50ab..e4297a71 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -1,21 +1,13 @@
class Tree
- include Linguist::BlobHelper
+ include Linguist::BlobHelper
attr_accessor :path, :tree, :project, :ref
- delegate :contents,
- :basename,
- :name,
- :data,
- :mime_type,
- :mode,
- :size,
- :text?,
- :colorize,
- to: :tree
+ delegate :contents, :basename, :name, :data, :mime_type,
+ :mode, :size, :text?, :colorize, to: :tree
def initialize(raw_tree, project, ref = nil, path = nil)
- @project, @ref, @path = project, ref, path,
- @tree = if path
+ @project, @ref, @path = project, ref, path
+ @tree = if path.present?
raw_tree / path.dup.force_encoding('ascii-8bit')
else
raw_tree
@@ -26,6 +18,10 @@ class Tree
tree.is_a?(Grit::Blob)
end
+ def invalid?
+ tree.nil?
+ end
+
def empty?
data.blank?
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 47876722..b0484698 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,122 +1,96 @@
class User < ActiveRecord::Base
-
include Account
devise :database_authenticatable, :token_authenticatable, :lockable,
:recoverable, :rememberable, :trackable, :validatable, :omniauthable
- attr_accessible :email, :password, :password_confirmation, :remember_me, :bio,
- :name, :projects_limit, :skype, :linkedin, :twitter, :dark_scheme,
- :theme_id, :force_random_password, :extern_uid, :provider
+ attr_accessible :email, :password, :password_confirmation, :remember_me, :bio, :name,
+ :skype, :linkedin, :twitter, :dark_scheme, :theme_id, :force_random_password,
+ :extern_uid, :provider, :as => [:default, :admin]
+ attr_accessible :projects_limit, :as => :admin
attr_accessor :force_random_password
- has_many :users_projects, dependent: :destroy
- has_many :projects, through: :users_projects
- has_many :my_own_projects, class_name: "Project", foreign_key: :owner_id
has_many :keys, dependent: :destroy
-
- has_many :events,
- class_name: "Event",
- foreign_key: :author_id,
- dependent: :destroy
-
- has_many :recent_events,
- class_name: "Event",
- foreign_key: :author_id,
- order: "id DESC"
-
- has_many :issues,
- foreign_key: :author_id,
- dependent: :destroy
-
- has_many :notes,
- foreign_key: :author_id,
- dependent: :destroy
-
- has_many :assigned_issues,
- class_name: "Issue",
- foreign_key: :assignee_id,
- dependent: :destroy
-
- has_many :merge_requests,
- foreign_key: :author_id,
- dependent: :destroy
-
- has_many :assigned_merge_requests,
- class_name: "MergeRequest",
- foreign_key: :assignee_id,
- dependent: :destroy
-
- validates :projects_limit,
- presence: true,
- numericality: {greater_than_or_equal_to: 0}
+ has_many :projects, through: :users_projects
+ has_many :users_projects, dependent: :destroy
+ has_many :issues, foreign_key: :author_id, dependent: :destroy
+ has_many :notes, foreign_key: :author_id, dependent: :destroy
+ has_many :merge_requests, foreign_key: :author_id, dependent: :destroy
+ has_many :my_own_projects, class_name: "Project", foreign_key: :owner_id
+ has_many :events, class_name: "Event", foreign_key: :author_id, dependent: :destroy
+ has_many :recent_events, class_name: "Event", foreign_key: :author_id, order: "id DESC"
+ has_many :assigned_issues, class_name: "Issue", foreign_key: :assignee_id, dependent: :destroy
+ has_many :assigned_merge_requests, class_name: "MergeRequest", foreign_key: :assignee_id, dependent: :destroy
validates :bio, length: { within: 0..255 }
-
validates :extern_uid, :allow_blank => true, :uniqueness => {:scope => :provider}
+ validates :projects_limit, presence: true, numericality: {greater_than_or_equal_to: 0}
+ before_validation :generate_password, on: :create
before_save :ensure_authentication_token
alias_attribute :private_token, :authentication_token
- scope :not_in_project, lambda { |project| where("id not in (:ids)", ids: project.users.map(&:id) ) }
+ # Scopes
+ scope :not_in_project, ->(project) { where("id not in (:ids)", ids: project.users.map(&:id) ) }
scope :admins, where(admin: true)
scope :blocked, where(blocked: true)
scope :active, where(blocked: false)
- before_validation :generate_password, on: :create
+ class << self
+ def filter filter_name
+ case filter_name
+ when "admins"; self.admins
+ when "blocked"; self.blocked
+ when "wop"; self.without_projects
+ else
+ self.active
+ end
+ end
+
+ def without_projects
+ where('id NOT IN (SELECT DISTINCT(user_id) FROM users_projects)')
+ end
+
+ def create_from_omniauth(auth, ldap = false)
+ gitlab_auth.create_from_omniauth(auth, ldap)
+ end
+
+ def find_or_new_for_omniauth(auth)
+ gitlab_auth.find_or_new_for_omniauth(auth)
+ end
+
+ def find_for_ldap_auth(auth, signed_in_resource = nil)
+ gitlab_auth.find_for_ldap_auth(auth, signed_in_resource)
+ end
+
+ def gitlab_auth
+ Gitlab::Auth.new
+ end
+
+ def search query
+ where("name LIKE :query or email LIKE :query", query: "%#{query}%")
+ end
+ end
def generate_password
if self.force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(8)
end
end
-
- def self.filter filter_name
- case filter_name
- when "admins"; self.admins
- when "blocked"; self.blocked
- when "wop"; self.without_projects
- else
- self.active
- end
- end
-
- def self.without_projects
- where('id NOT IN (SELECT DISTINCT(user_id) FROM users_projects)')
- end
-
- def self.create_from_omniauth(auth, ldap = false)
- gitlab_auth.create_from_omniauth(auth, ldap)
- end
-
- def self.find_or_new_for_omniauth(auth)
- gitlab_auth.find_or_new_for_omniauth(auth)
- end
-
- def self.find_for_ldap_auth(auth, signed_in_resource = nil)
- gitlab_auth.find_for_ldap_auth(auth, signed_in_resource)
- end
-
- def self.gitlab_auth
- Gitlab::Auth.new
- end
-
- def self.search query
- where("name like :query or email like :query", query: "%#{query}%")
- end
end
+
# == Schema Information
#
# Table name: users
#
-# id :integer(4) not null, primary key
+# id :integer not null, primary key
# email :string(255) default(""), not null
# encrypted_password :string(128) default(""), not null
# reset_password_token :string(255)
# reset_password_sent_at :datetime
# remember_created_at :datetime
-# sign_in_count :integer(4) default(0)
+# sign_in_count :integer default(0)
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# current_sign_in_ip :string(255)
@@ -124,14 +98,19 @@ end
# created_at :datetime not null
# updated_at :datetime not null
# name :string(255)
-# admin :boolean(1) default(FALSE), not null
-# projects_limit :integer(4) default(10)
+# admin :boolean default(FALSE), not null
+# projects_limit :integer default(10)
# skype :string(255) default(""), not null
# linkedin :string(255) default(""), not null
# twitter :string(255) default(""), not null
# authentication_token :string(255)
-# dark_scheme :boolean(1) default(FALSE), not null
-# theme_id :integer(4) default(1), not null
+# dark_scheme :boolean default(FALSE), not null
+# theme_id :integer default(1), not null
# bio :string(255)
-# blocked :boolean(1) default(FALSE), not null
+# blocked :boolean default(FALSE), not null
+# failed_attempts :integer default(0)
+# locked_at :datetime
+# extern_uid :string(255)
+# provider :string(255)
#
+
diff --git a/app/models/users_project.rb b/app/models/users_project.rb
index ce64a10f..e336fac3 100644
--- a/app/models/users_project.rb
+++ b/app/models/users_project.rb
@@ -6,70 +6,72 @@ class UsersProject < ActiveRecord::Base
DEVELOPER = 30
MASTER = 40
+ attr_accessible :user, :user_id, :project_access
+
belongs_to :user
belongs_to :project
- attr_protected :project_id, :project
-
after_save :update_repository
after_destroy :update_repository
- validates_uniqueness_of :user_id, scope: [:project_id], message: "already exists in project"
- validates_presence_of :user_id
- validates_presence_of :project_id
+ validates :user, presence: true
+ validates :user_id, uniqueness: { :scope => [:project_id], message: "already exists in project" }
+ validates :project, presence: true
delegate :name, :email, to: :user, prefix: true
- def self.bulk_delete(project, user_ids)
- UsersProject.transaction do
- UsersProject.where(:user_id => user_ids, :project_id => project.id).each do |users_project|
- users_project.destroy
+ class << self
+ def bulk_delete(project, user_ids)
+ UsersProject.transaction do
+ UsersProject.where(:user_id => user_ids, :project_id => project.id).each do |users_project|
+ users_project.destroy
+ end
end
end
- end
- def self.bulk_update(project, user_ids, project_access)
- UsersProject.transaction do
- UsersProject.where(:user_id => user_ids, :project_id => project.id).each do |users_project|
- users_project.project_access = project_access
- users_project.save
+ def bulk_update(project, user_ids, project_access)
+ UsersProject.transaction do
+ UsersProject.where(:user_id => user_ids, :project_id => project.id).each do |users_project|
+ users_project.project_access = project_access
+ users_project.save
+ end
end
end
- end
- def self.bulk_import(project, user_ids, project_access)
- UsersProject.transaction do
- user_ids.each do |user_id|
- users_project = UsersProject.new(
- project_access: project_access,
- user_id: user_id
- )
- users_project.project = project
- users_project.save
+ def bulk_import(project, user_ids, project_access)
+ UsersProject.transaction do
+ user_ids.each do |user_id|
+ users_project = UsersProject.new(
+ project_access: project_access,
+ user_id: user_id
+ )
+ users_project.project = project
+ users_project.save
+ end
end
end
- end
- def self.user_bulk_import(user, project_ids, project_access)
- UsersProject.transaction do
- project_ids.each do |project_id|
- users_project = UsersProject.new(
- project_access: project_access,
- )
- users_project.project_id = project_id
- users_project.user_id = user.id
- users_project.save
+ def user_bulk_import(user, project_ids, project_access)
+ UsersProject.transaction do
+ project_ids.each do |project_id|
+ users_project = UsersProject.new(
+ project_access: project_access,
+ )
+ users_project.project_id = project_id
+ users_project.user_id = user.id
+ users_project.save
+ end
end
end
- end
- def self.access_roles
- {
- "Guest" => GUEST,
- "Reporter" => REPORTER,
- "Developer" => DEVELOPER,
- "Master" => MASTER
- }
+ def access_roles
+ {
+ "Guest" => GUEST,
+ "Reporter" => REPORTER,
+ "Developer" => DEVELOPER,
+ "Master" => MASTER
+ }
+ end
end
def role_access
@@ -88,15 +90,16 @@ class UsersProject < ActiveRecord::Base
self.class.access_roles.invert[self.project_access]
end
end
+
# == Schema Information
#
# Table name: users_projects
#
-# id :integer(4) not null, primary key
-# user_id :integer(4) not null
-# project_id :integer(4) not null
+# id :integer not null, primary key
+# user_id :integer not null
+# project_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
-# project_access :integer(4) default(0), not null
+# project_access :integer default(0), not null
#
diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb
index 76efa501..db773c55 100644
--- a/app/models/web_hook.rb
+++ b/app/models/web_hook.rb
@@ -1,37 +1,37 @@
class WebHook < ActiveRecord::Base
include HTTParty
+ attr_accessible :url
+
# HTTParty timeout
default_timeout 10
- validates :url,
- presence: true,
- format: {
- with: URI::regexp(%w(http https)),
- message: "should be a valid url" }
+ validates :url, presence: true,
+ format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
def execute(data)
parsed_url = URI.parse(url)
if parsed_url.userinfo.blank?
WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" })
else
- post_url = url.gsub(parsed_url.userinfo+"@", "")
+ post_url = url.gsub("#{parsed_url.userinfo}@", "")
WebHook.post(post_url,
body: data.to_json,
- headers: { "Content-Type" => "application/json" },
+ headers: {"Content-Type" => "application/json"},
basic_auth: {username: parsed_url.user, password: parsed_url.password})
end
end
-
end
+
# == Schema Information
#
# Table name: web_hooks
#
-# id :integer(4) not null, primary key
+# id :integer not null, primary key
# url :string(255)
-# project_id :integer(4)
+# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
+# type :string(255) default("ProjectHook")
#
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index ebb1ff49..b1f41d63 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -1,10 +1,13 @@
class Wiki < ActiveRecord::Base
+ attr_accessible :title, :content, :slug
+
belongs_to :project
belongs_to :user
has_many :notes, as: :noteable, dependent: :destroy
- validates :content, :title, :user_id, presence: true
- validates :title, length: 1..250
+ validates :content, presence: true
+ validates :user, presence: true
+ validates :title, presence: true, length: 1..250
before_update :set_slug
@@ -14,33 +17,33 @@ class Wiki < ActiveRecord::Base
protected
+ def self.regenerate_from wiki
+ regenerated_field = [:slug, :content, :title]
+
+ new_wiki = Wiki.new
+ regenerated_field.each do |field|
+ new_wiki.send("#{field}=", wiki.send(field))
+ end
+ new_wiki
+ end
+
def set_slug
self.slug = self.title.parameterize
end
- class << self
- def regenerate_from wiki
- regenerated_field = [:slug, :content, :title]
-
- new_wiki = Wiki.new
- regenerated_field.each do |field|
- new_wiki.send("#{field}=", wiki.send(field))
- end
- new_wiki
- end
- end
end
+
# == Schema Information
#
# Table name: wikis
#
-# id :integer(4) not null, primary key
+# id :integer not null, primary key
# title :string(255)
# content :text
-# project_id :integer(4)
+# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# slug :string(255)
-# user_id :integer(4)
+# user_id :integer
#
diff --git a/app/observers/mailer_observer.rb b/app/observers/mailer_observer.rb
deleted file mode 100644
index 331aaa35..00000000
--- a/app/observers/mailer_observer.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-class MailerObserver < ActiveRecord::Observer
- observe :note, :merge_request
- cattr_accessor :current_user
-
- def after_create(model)
- new_note(model) if model.kind_of?(Note)
- new_merge_request(model) if model.kind_of?(MergeRequest)
- end
-
- def after_update(model)
- changed_merge_request(model) if model.kind_of?(MergeRequest)
- end
-
- protected
-
- def new_note(note)
- if note.notify
- # Notify whole team except author of note
- notify_note(note)
- elsif note.notify_author
- # Notify only author of resource
- Notify.note_commit_email(note.commit_author.id, note.id).deliver
- else
- # Otherwise ignore it
- nil
- end
- end
-
- def notify_note note
- # reject author of note from mail list
- users = note.project.users.reject { |u| u.id == current_user.id }
-
- users.each do |u|
- case note.noteable_type
- when "Commit"; Notify.note_commit_email(u.id, note.id).deliver
- when "Issue"; Notify.note_issue_email(u.id, note.id).deliver
- when "Wiki"; Notify.note_wiki_email(u.id, note.id).deliver
- when "MergeRequest"; Notify.note_merge_request_email(u.id, note.id).deliver
- when "Snippet"; true
- else
- Notify.note_wall_email(u.id, note.id).deliver
- end
- end
- end
-
- def new_merge_request(merge_request)
- if merge_request.assignee && merge_request.assignee != current_user
- Notify.new_merge_request_email(merge_request.id).deliver
- end
- end
-
- def changed_merge_request(merge_request)
- status_notify_and_comment merge_request, :reassigned_merge_request_email
- end
-
- # This method used for Issues & Merge Requests
- #
- # It create a comment for Issue or MR if someone close/reopen.
- # It also notify via email if assignee was changed
- #
- def status_notify_and_comment target, mail_method
- # If assigne changed - notify to recipients
- if target.assignee_id_changed?
- recipients_ids = target.assignee_id_was, target.assignee_id
- recipients_ids.delete current_user.id
-
- recipients_ids.each do |recipient_id|
- Notify.send(mail_method, recipient_id, target.id, target.assignee_id_was).deliver
- end
- end
-
- # Create comment about status changed
- if target.closed_changed?
- note = Note.new(noteable: target, project: target.project)
- note.author = current_user
- note.note = "_Status changed to #{target.closed ? 'closed' : 'reopened'}_"
- note.save()
- end
- end
-end
diff --git a/app/observers/merge_request_observer.rb b/app/observers/merge_request_observer.rb
new file mode 100644
index 00000000..c4040f15
--- /dev/null
+++ b/app/observers/merge_request_observer.rb
@@ -0,0 +1,31 @@
+class MergeRequestObserver < ActiveRecord::Observer
+ cattr_accessor :current_user
+
+ def after_create(merge_request)
+ if merge_request.assignee && merge_request.assignee != current_user
+ Notify.new_merge_request_email(merge_request.id).deliver
+ end
+ end
+
+ def after_update(merge_request)
+ send_reassigned_email(merge_request) if merge_request.is_being_reassigned?
+
+ status = nil
+ status = 'closed' if merge_request.is_being_closed?
+ status = 'reopened' if merge_request.is_being_reopened?
+ if status
+ Note.create_status_change_note(merge_request, current_user, status)
+ end
+ end
+
+ protected
+
+ def send_reassigned_email(merge_request)
+ recipients_ids = merge_request.assignee_id_was, merge_request.assignee_id
+ recipients_ids.delete current_user.id
+
+ recipients_ids.each do |recipient_id|
+ Notify.reassigned_merge_request_email(recipient_id, merge_request.id, merge_request.assignee_id_was).deliver
+ end
+ end
+end
diff --git a/app/observers/note_observer.rb b/app/observers/note_observer.rb
new file mode 100644
index 00000000..083aa705
--- /dev/null
+++ b/app/observers/note_observer.rb
@@ -0,0 +1,37 @@
+class NoteObserver < ActiveRecord::Observer
+
+ def after_create(note)
+ send_notify_mails(note)
+ end
+
+ protected
+
+ def send_notify_mails(note)
+ if note.notify
+ notify_team(note)
+ elsif note.notify_author
+ # Notify only author of resource
+ Notify.note_commit_email(note.commit_author.id, note.id).deliver
+ else
+ # Otherwise ignore it
+ nil
+ end
+ end
+
+ # Notifies the whole team except the author of note
+ def notify_team(note)
+ # Note: wall posts are not "attached" to anything, so fall back to "Wall"
+ noteable_type = note.noteable_type || "Wall"
+ notify_method = "note_#{noteable_type.underscore}_email".to_sym
+
+ if Notify.respond_to? notify_method
+ team_without_note_author(note).map do |u|
+ Notify.send(notify_method, u.id, note.id).deliver
+ end
+ end
+ end
+
+ def team_without_note_author(note)
+ note.project.users.reject { |u| u.id == note.author.id }
+ end
+end
diff --git a/app/observers/users_project_observer.rb b/app/observers/users_project_observer.rb
index 0512e606..0c9c2b26 100644
--- a/app/observers/users_project_observer.rb
+++ b/app/observers/users_project_observer.rb
@@ -1,7 +1,10 @@
class UsersProjectObserver < ActiveRecord::Observer
- def after_create(users_project)
+ def after_commit(users_project)
+ return if users_project.destroyed?
Notify.project_access_granted_email(users_project.id).deliver
+ end
+ def after_create(users_project)
Event.create(
project_id: users_project.project.id,
action: Event::Joined,
@@ -9,10 +12,6 @@ class UsersProjectObserver < ActiveRecord::Observer
)
end
- def after_update(users_project)
- Notify.project_access_granted_email(users_project.id).deliver
- end
-
def after_destroy(users_project)
Event.create(
project_id: users_project.project.id,
diff --git a/app/roles/account.rb b/app/roles/account.rb
index b8c445a3..21545b91 100644
--- a/app/roles/account.rb
+++ b/app/roles/account.rb
@@ -22,6 +22,10 @@ module Account
projects_limit > my_own_projects.count
end
+ def can_create_group?
+ is_admin?
+ end
+
def last_activity_project
projects.first
end
@@ -41,7 +45,7 @@ module Account
# Remove user from all projects and
# set blocked attribute to true
def block
- users_projects.all.each do |membership|
+ users_projects.find_each do |membership|
return false unless membership.destroy
end
diff --git a/app/roles/authority.rb b/app/roles/authority.rb
index 9d9153db..e0796d5f 100644
--- a/app/roles/authority.rb
+++ b/app/roles/authority.rb
@@ -2,12 +2,12 @@ module Authority
# Compatible with all access rights
# Should be rewrited for new access rights
def add_access(user, *access)
- access = if access.include?(:admin)
- { project_access: UsersProject::MASTER }
+ access = if access.include?(:admin)
+ { project_access: UsersProject::MASTER }
elsif access.include?(:write)
- { project_access: UsersProject::DEVELOPER }
+ { project_access: UsersProject::DEVELOPER }
else
- { project_access: UsersProject::REPORTER }
+ { project_access: UsersProject::REPORTER }
end
opts = { user: user }
opts.merge!(access)
@@ -53,6 +53,6 @@ module Authority
end
def master_access_for?(user)
- !users_projects.where(user_id: user.id, project_access: [UsersProject::MASTER]).empty? || owner_id == user.id
+ !users_projects.where(user_id: user.id, project_access: [UsersProject::MASTER]).empty?
end
end
diff --git a/app/roles/issue_commonality.rb b/app/roles/issue_commonality.rb
index ac972a70..2d10bfec 100644
--- a/app/roles/issue_commonality.rb
+++ b/app/roles/issue_commonality.rb
@@ -3,24 +3,21 @@ module IssueCommonality
extend ActiveSupport::Concern
included do
- attr_protected :author, :author_id, :project, :project_id
-
belongs_to :project
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
has_many :notes, as: :noteable, dependent: :destroy
- validates_presence_of :project_id
- validates_presence_of :author_id
-
- validates :title,
- presence: true,
- length: { within: 0..255 }
+ validates :project, presence: true
+ validates :author, presence: true
+ validates :title, presence: true, length: { within: 0..255 }
validates :closed, inclusion: { in: [true, false] }
scope :opened, where(closed: false)
scope :closed, where(closed: true)
- scope :assigned, lambda { |u| where(assignee_id: u.id)}
+ scope :of_group, ->(group) { where(project_id: group.project_ids) }
+ scope :assigned, ->(u) { where(assignee_id: u.id)}
+ scope :recent, order("created_at DESC")
delegate :name,
:email,
@@ -49,4 +46,21 @@ module IssueCommonality
def new?
today? && created_at == updated_at
end
+
+ def is_assigned?
+ !!assignee_id
+ end
+
+ def is_being_reassigned?
+ assignee_id_changed?
+ end
+
+ def is_being_closed?
+ closed_changed? && closed
+ end
+
+ def is_being_reopened?
+ closed_changed? && !closed
+ end
+
end
diff --git a/app/roles/push_event.rb b/app/roles/push_event.rb
index a607f212..8ce71b54 100644
--- a/app/roles/push_event.rb
+++ b/app/roles/push_event.rb
@@ -5,11 +5,11 @@ module PushEvent
false
end
- def tag?
+ def tag?
data[:ref]["refs/tags"]
end
- def branch?
+ def branch?
data[:ref]["refs/heads"]
end
@@ -25,7 +25,7 @@ module PushEvent
commit_to =~ /^00000/
end
- def md_ref?
+ def md_ref?
!(rm_ref? || new_ref?)
end
@@ -37,7 +37,7 @@ module PushEvent
data[:after]
end
- def ref_name
+ def ref_name
if tag?
tag_name
else
@@ -70,7 +70,7 @@ module PushEvent
if new_ref?
"pushed new"
elsif rm_ref?
- "removed #{ref_type}"
+ "deleted"
else
"pushed to"
end
diff --git a/app/roles/repository.rb b/app/roles/repository.rb
index 01156ac1..882ec310 100644
--- a/app/roles/repository.rb
+++ b/app/roles/repository.rb
@@ -32,6 +32,10 @@ module Repository
Commit.commits(repo, ref, path, limit, offset)
end
+ def last_commit_for(ref, path = nil)
+ commits(ref, path, 1).first
+ end
+
def commits_between(from, to)
Commit.commits_between(repo, from, to)
end
@@ -45,8 +49,29 @@ module Repository
File.exists?(hook_file)
end
+ # Returns an Array of branch names
+ def branch_names
+ repo.branches.collect(&:name).sort
+ end
+
+ # Returns an Array of Branches
+ def branches
+ repo.branches.sort_by(&:name)
+ end
+
+ # Returns an Array of tag names
+ def tag_names
+ repo.tags.collect(&:name).sort.reverse
+ end
+
+ # Returns an Array of Tags
def tags
- repo.tags.map(&:name).sort.reverse
+ repo.tags.sort_by(&:name).reverse
+ end
+
+ # Returns an Array of branch and tag names
+ def ref_names
+ [branch_names + tag_names].flatten
end
def repo
@@ -79,14 +104,6 @@ module Repository
@heads ||= repo.heads
end
- def branches_names
- heads.map(&:name)
- end
-
- def ref_names
- [branches_names + tags].flatten
- end
-
def tree(fcommit, path = nil)
fcommit = commit if fcommit == :head
tree = fcommit.tree
@@ -109,14 +126,12 @@ module Repository
# - If two or more branches are present, returns the one that has a name
# matching root_ref (default_branch or 'master' if default_branch is nil)
def discover_default_branch
- branches = heads.collect(&:name)
-
- if branches.length == 0
+ if branch_names.length == 0
nil
- elsif branches.length == 1
- branches.first
+ elsif branch_names.length == 1
+ branch_names.first
else
- branches.select { |v| v == root_ref }.first
+ branch_names.select { |v| v == root_ref }.first
end
end
@@ -144,7 +159,7 @@ module Repository
# Build file path
file_name = self.code + "-" + commit.id.to_s + ".tar.gz"
- storage_path = File.join(Rails.root, "tmp", "repositories", self.code)
+ storage_path = Rails.root.join("tmp", "repositories", self.code)
file_path = File.join(storage_path, file_name)
# Put files into a directory before archiving
@@ -166,4 +181,9 @@ module Repository
def http_url_to_repo
http_url = [Gitlab.config.url, "/", path, ".git"].join('')
end
+
+ # Check if current branch name is marked as protected in the system
+ def protected_branch? branch_name
+ protected_branches.map(&:name).include?(branch_name)
+ end
end
diff --git a/app/roles/static_model.rb b/app/roles/static_model.rb
index d26c8f47..5b64be1f 100644
--- a/app/roles/static_model.rb
+++ b/app/roles/static_model.rb
@@ -25,6 +25,10 @@ module StaticModel
id
end
+ def new_record?
+ false
+ end
+
def persisted?
false
end
@@ -32,4 +36,12 @@ module StaticModel
def destroyed?
false
end
+
+ def ==(other)
+ if other.is_a? StaticModel
+ id == other.id
+ else
+ super
+ end
+ end
end
diff --git a/app/roles/team.rb b/app/roles/team.rb
index 8aef405a..a7ba0588 100644
--- a/app/roles/team.rb
+++ b/app/roles/team.rb
@@ -1,7 +1,7 @@
module Team
- def team_member_by_name_or_email(email = nil, name = nil)
- user = users.where("email like ? or name like ?", email, name).first
- users_projects.find_by_user_id(user.id) if user
+ def team_member_by_name_or_email(name = nil, email = nil)
+ user = users.where("name like ? or email like ?", name, email).first
+ users_projects.where(user: user) if user
end
# Get Team Member record by user id
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
new file mode 100644
index 00000000..e85cce66
--- /dev/null
+++ b/app/views/admin/groups/_form.html.haml
@@ -0,0 +1,19 @@
+= form_for [:admin, @group] do |f|
+ - if @group.errors.any?
+ .alert-message.block-message.error
+ %span= @group.errors.full_messages.first
+ .clearfix.group_name_holder
+ = f.label :name do
+ Group name is
+ .input
+ = f.text_field :name, placeholder: "Example Group", class: "xxlarge"
+ .clearfix
+ = f.label :code do
+ URL
+ .input
+ .input-prepend
+ %span.add-on= web_app_url + 'groups/'
+ = f.text_field :code, placeholder: "example"
+
+ .form-actions
+ = f.submit 'Save group', class: "btn save-btn"
diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml
new file mode 100644
index 00000000..9904122c
--- /dev/null
+++ b/app/views/admin/groups/edit.html.haml
@@ -0,0 +1,3 @@
+%h3.page_title Edit Group
+%br
+= render 'form'
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
new file mode 100644
index 00000000..25efc9ee
--- /dev/null
+++ b/app/views/admin/groups/index.html.haml
@@ -0,0 +1,25 @@
+= render 'admin/shared/projects_head'
+%h3.page_title
+ Groups
+ = link_to 'New Group', new_admin_group_path, class: "btn small right"
+%br
+= form_tag admin_groups_path, method: :get, class: 'form-inline' do
+ = text_field_tag :name, params[:name], class: "xlarge"
+ = submit_tag "Search", class: "btn submit primary"
+
+%table
+ %thead
+ %th Name
+ %th Code
+ %th Projects
+ %th Edit
+ %th.cred Danger Zone!
+
+ - @groups.each do |group|
+ %tr
+ %td= link_to group.name, [:admin, group]
+ %td= group.code
+ %td= group.projects.count
+ %td= link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn small"
+ %td.bgred= link_to 'Destroy', [:admin, group], confirm: "REMOVE #{group.name}? Are you sure?", method: :delete, class: "btn small danger"
+= paginate @groups, theme: "admin"
diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml
new file mode 100644
index 00000000..d6b6ea15
--- /dev/null
+++ b/app/views/admin/groups/new.html.haml
@@ -0,0 +1,3 @@
+%h3.page_title New Group
+%br
+= render 'form'
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
new file mode 100644
index 00000000..309a10e5
--- /dev/null
+++ b/app/views/admin/groups/show.html.haml
@@ -0,0 +1,52 @@
+= render 'admin/shared/projects_head'
+%h3.page_title
+ Group: #{@group.name}
+ = link_to edit_admin_group_path(@group), class: "btn right" do
+ %i.icon-edit
+ Edit
+
+%br
+%table.zebra-striped
+ %thead
+ %tr
+ %th Group
+ %th
+ %tr
+ %td
+ %b
+ Name:
+ %td
+ = @group.name
+ %tr
+ %td
+ %b
+ Code:
+ %td
+ = @group.code
+ %tr
+ %td
+ %b
+ Owner:
+ %td
+ = @group.owner_name
+.ui-box
+ %h5
+ Projects
+ %small
+ (#{@group.projects.count})
+ %ul.unstyled
+ - @group.projects.each do |project|
+ %li.wll
+ %strong
+ = link_to project.name, [:admin, project]
+ .right
+ = link_to 'Remove from group', remove_project_admin_group_path(@group, project_id: project.id), confirm: 'Are you sure?', method: :delete, class: "btn danger small"
+ .clearfix
+
+%br
+%h3 Add new project
+%br
+= form_tag project_update_admin_group_path(@group), class: "bulk_import", method: :put do
+ = select_tag :project_ids, options_from_collection_for_select(@projects , :id, :name), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5'
+ .form-actions
+ = submit_tag 'Add', class: "btn primary"
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index bd38ae72..3335fce0 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,3 +1,4 @@
+= render 'admin/shared/projects_head'
%h3.page_title
Projects
= link_to 'New Project', new_admin_project_path, class: "btn small right"
@@ -11,7 +12,6 @@
%th Name
%th Path
%th Team Members
- %th Post Receive
%th Last Commit
%th Edit
%th.cred Danger Zone!
@@ -21,7 +21,6 @@
%td= link_to project.name, [:admin, project]
%td= project.path
%td= project.users_projects.count
- %td= check_box_tag :post_receive_file, 1, project.has_post_receive_file?, disabled: true
%td= last_commit(project)
%td= link_to 'Edit', edit_admin_project_path(project), id: "edit_#{dom_id(project)}", class: "btn small"
%td.bgred= link_to 'Destroy', [:admin, project], confirm: "REMOVE #{project.name}? Are you sure?", method: :delete, class: "btn small danger"
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 63987410..f85b6e4b 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,6 +1,18 @@
-%h3
- = @admin_project.name
- = link_to 'Edit', edit_admin_project_path(@admin_project), class: "btn right small"
+= render 'admin/shared/projects_head'
+%h3.page_title
+ Project: #{@admin_project.name}
+ = link_to edit_admin_project_path(@admin_project), class: "btn right" do
+ %i.icon-edit
+ Edit
+
+- if !@admin_project.has_post_receive_file? && @admin_project.commit
+ %br
+ .alert.alert-error
+ %span
+ %strong Important!
+ Project has commits but missing post-receive file.
+ %br
+ If you exported project manually - copy post-receive hook to bare repository
%br
%table.zebra-striped
@@ -56,7 +68,7 @@
%tr
%td
= link_to tm.user_name, admin_user_path(tm.user)
- %td= select_tag :tm_project_access, options_for_select(Project.access_options, tm.project_access), class: "medium project-access-select", disabled: :disabled
+ %td= tm.project_access_human
%td= link_to 'Edit Access', edit_admin_team_member_path(tm), class: "btn small"
%td= link_to 'Remove from team', admin_team_member_path(tm), confirm: 'Are you sure?', method: :delete, class: "btn danger small"
diff --git a/app/views/admin/shared/_projects_head.html.haml b/app/views/admin/shared/_projects_head.html.haml
new file mode 100644
index 00000000..3f5c34c5
--- /dev/null
+++ b/app/views/admin/shared/_projects_head.html.haml
@@ -0,0 +1,5 @@
+%ul.nav.nav-tabs
+ = nav_link(controller: :projects) do
+ = link_to 'Projects', admin_projects_path, class: "tab"
+ = nav_link(controller: :groups) do
+ = link_to 'Groups', admin_groups_path, class: "tab"
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 731916e9..e73f4d10 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -1,10 +1,12 @@
-%h3
- = @admin_user.name
+%h3.page_title
+ User: #{@admin_user.name}
- if @admin_user.blocked
%small Blocked
- if @admin_user.admin
%small Administrator
- = link_to 'Edit', edit_admin_user_path(@admin_user), class: "btn small right"
+ = link_to edit_admin_user_path(@admin_user), class: "btn right" do
+ %i.icon-edit
+ Edit
%br
@@ -94,6 +96,6 @@
- project = tm.project
%tr
%td= link_to project.name, admin_project_path(project)
- %td= select_tag :tm_project_access, options_for_select(Project.access_options, tm.project_access), class: "medium project-access-select", disabled: :disabled
+ %td= tm.project_access_human
%td= link_to 'Edit Access', edit_admin_team_member_path(tm), class: "btn small"
%td= link_to 'Remove from team', admin_team_member_path(tm), confirm: 'Are you sure?', method: :delete, class: "btn small danger"
diff --git a/app/views/refs/_head.html.haml b/app/views/blame/_head.html.haml
similarity index 72%
rename from app/views/refs/_head.html.haml
rename to app/views/blame/_head.html.haml
index 3592f573..175719b1 100644
--- a/app/views/refs/_head.html.haml
+++ b/app/views/blame/_head.html.haml
@@ -1,11 +1,10 @@
%ul.nav.nav-tabs
%li
= render partial: 'shared/ref_switcher', locals: {destination: 'tree', path: params[:path]}
- %li{class: "#{'active' if (controller.controller_name == "refs") }"}
- = link_to tree_project_ref_path(@project, @ref) do
- Source
+ = nav_link(controller: :refs) do
+ = link_to 'Source', project_tree_path(@project, @ref)
%li.right
.input-prepend.project_clone_holder
%button{class: "btn small active", :"data-clone" => @project.ssh_url_to_repo} SSH
- %button{class: "btn small", :"data-clone" => @project.http_url_to_repo} HTTP
+ %button{class: "btn small", :"data-clone" => @project.http_url_to_repo}= Gitlab.config.web_protocol.upcase
= text_field_tag :project_clone, @project.url_to_repo, class: "one_click_select span5"
diff --git a/app/views/refs/blame.html.haml b/app/views/blame/show.html.haml
similarity index 57%
rename from app/views/refs/blame.html.haml
rename to app/views/blame/show.html.haml
index eb66f597..5c3231e2 100644
--- a/app/views/refs/blame.html.haml
+++ b/app/views/blame/show.html.haml
@@ -1,10 +1,10 @@
= render "head"
-#tree-holder
+#tree-holder.tree-holder
%ul.breadcrumb
%li
%span.arrow
- = link_to tree_project_ref_path(@project, @ref, path: nil) do
+ = link_to project_tree_path(@project, @ref) do
= @project.name
- @tree.breadcrumbs(6) do |link|
\/
@@ -15,12 +15,9 @@
.file_title
%i.icon-file
%span.file_name
- = @tree.name
- %small blame
- %span.options
- = link_to "raw", blob_project_ref_path(@project, @ref, path: params[:path]), class: "btn very_small", target: "_blank"
- = link_to "history", project_commits_path(@project, path: params[:path], ref: @ref), class: "btn very_small"
- = link_to "source", tree_file_project_ref_path(@project, @ref, path: params[:path]), class: "btn very_small"
+ = @tree.name.force_encoding('utf-8')
+ %small= number_to_human_size @tree.size
+ %span.options= render "tree/blob_actions"
.file_content.blame
%table
- @blame.each do |commit, lines|
@@ -32,8 +29,8 @@
= commit.author_name
%td.blame_commit
- %code= link_to commit.short_id, project_commit_path(@project, id: commit.id)
- = link_to_gfm truncate(commit.title, length: 30), project_commit_path(@project, id: commit.id), class: "row_title" rescue "--broken encoding"
+ %code= link_to commit.short_id, project_commit_path(@project, commit)
+ = link_to_gfm truncate(commit.title, length: 30), project_commit_path(@project, commit), class: "row_title" rescue "--broken encoding"
%td.lines
= preserve do
%pre
diff --git a/app/views/commit/show.html.haml b/app/views/commit/show.html.haml
new file mode 100644
index 00000000..d12fff96
--- /dev/null
+++ b/app/views/commit/show.html.haml
@@ -0,0 +1,10 @@
+= render "commits/commit_box"
+= render "commits/diffs", diffs: @commit.diffs
+= render "notes/notes_with_form", tid: @commit.id, tt: "commit"
+= render "notes/per_line_form"
+
+
+:javascript
+ $(function(){
+ PerLineNotes.init();
+ });
diff --git a/app/views/commit/show.patch.erb b/app/views/commit/show.patch.erb
new file mode 100644
index 00000000..ce1c3d02
--- /dev/null
+++ b/app/views/commit/show.patch.erb
@@ -0,0 +1 @@
+<%= @commit.to_patch %>
diff --git a/app/views/commits/_commit.html.haml b/app/views/commits/_commit.html.haml
index 61371d14..9abadc5d 100644
--- a/app/views/commits/_commit.html.haml
+++ b/app/views/commits/_commit.html.haml
@@ -1,16 +1,22 @@
%li.commit
.browse_code_link_holder
%p
- %strong= link_to "Browse Code »", tree_project_ref_path(@project, commit.id), class: "right"
+ %strong= link_to "Browse Code »", project_tree_path(@project, commit), class: "right"
%p
- = link_to commit.short_id(8), project_commit_path(@project, id: commit.id), class: "commit_short_id"
+ = link_to commit.short_id(8), project_commit_path(@project, commit), class: "commit_short_id"
%strong.commit-author-name= commit.author_name
%span.dash –
= image_tag gravatar_icon(commit.author_email), class: "avatar", width: 16
- = link_to_gfm truncate(commit.title, length: 50), project_commit_path(@project, id: commit.id), class: "row_title"
+ = link_to_gfm truncate(commit.title, length: 50), project_commit_path(@project, commit.id), class: "row_title"
%span.committed_ago
= time_ago_in_words(commit.committed_date)
ago
+ %span.notes_count
+ - notes = @project.commit_notes(commit) + @project.commit_line_notes(commit)
+ - if notes.any?
+ %span.btn.small.disabled.grouped
+ %i.icon-comment
+ = notes.count
diff --git a/app/views/commits/_commit_box.html.haml b/app/views/commits/_commit_box.html.haml
index 572337de..ece0df22 100644
--- a/app/views/commits/_commit_box.html.haml
+++ b/app/views/commits/_commit_box.html.haml
@@ -5,10 +5,10 @@
%span.btn.disabled.grouped
%i.icon-comment
= @notes_count
- = link_to patch_project_commit_path(@project, @commit.id), class: "btn small grouped" do
+ = link_to project_commit_path(@project, @commit, format: :patch), class: "btn small grouped" do
%i.icon-download-alt
Get Patch
- = link_to tree_project_ref_path(@project, @commit.id), class: "browse-button primary grouped" do
+ = link_to project_tree_path(@project, @commit), class: "browse-button primary grouped" do
%strong Browse Code »
%h3.commit-title.page_title
= gfm escape_once(@commit.title)
@@ -17,7 +17,7 @@
= gfm escape_once(@commit.description)
.commit-info
.row
- .span4
+ .span5
= image_tag gravatar_icon(@commit.author_email, 40), class: "avatar"
.author
%strong= @commit.author_name
@@ -31,10 +31,10 @@
committed
%time{title: @commit.committed_date.stamp("Aug 21, 2011 9:23pm")}
#{time_ago_in_words(@commit.committed_date)} ago
- .span7.right
+ .span6.right
.sha-block
%span.cgray commit
- %code= @commit.id
+ %code.label_commit= @commit.id
.sha-block
%span.cgray= pluralize(@commit.parents.count, "parent")
- @commit.parents.each do |parent|
diff --git a/app/views/commits/_diffs.html.haml b/app/views/commits/_diffs.html.haml
index b590d64c..026fe27e 100644
--- a/app/views/commits/_diffs.html.haml
+++ b/app/views/commits/_diffs.html.haml
@@ -5,7 +5,7 @@
%p To prevent performance issue we rejected diff information.
%p
But if you still want to see diff
- = link_to "click this link", project_commit_path(@project, @commit.id, force_show_diff: true), class: "dark"
+ = link_to "click this link", project_commit_path(@project, @commit, force_show_diff: true), class: "dark"
%p.cgray
Showing #{pluralize(diffs.count, "changed file")}
@@ -24,7 +24,7 @@
%i.icon-file
%span{id: "#{diff.old_path}"}= diff.old_path
- else
- = link_to tree_file_project_ref_path(@project, @commit.id, diff.new_path) do
+ = link_to project_tree_path(@project, tree_join(@commit.id, diff.new_path)) do
%i.icon-file
%span{id: "#{diff.new_path}"}= diff.new_path
%br/
diff --git a/app/views/commits/_head.html.haml b/app/views/commits/_head.html.haml
index a8111a72..c001c2f7 100644
--- a/app/views/commits/_head.html.haml
+++ b/app/views/commits/_head.html.haml
@@ -1,23 +1,23 @@
%ul.nav.nav-tabs
%li= render partial: 'shared/ref_switcher', locals: {destination: 'commits'}
- %li{class: "#{'active' if current_page?(project_commits_path(@project)) }"}
- = link_to project_commits_path(@project) do
- Commits
- %li{class: "#{'active' if current_page?(compare_project_commits_path(@project)) }"}
- = link_to compare_project_commits_path(@project) do
- Compare
- %li{class: "#{branches_tab_class}"}
+
+ = nav_link(controller: [:commit, :commits]) do
+ = link_to 'Commits', project_commits_path(@project, @project.root_ref)
+ = nav_link(controller: :compare) do
+ = link_to 'Compare', project_compare_index_path(@project)
+
+ = nav_link(html_options: {class: branches_tab_class}) do
= link_to project_repository_path(@project) do
Branches
- %span.badge= @project.repo.branch_count
+ %span.badge= @project.branches.length
- %li{class: "#{'active' if current_page?(tags_project_repository_path(@project)) }"}
+ = nav_link(controller: :repositories, action: :tags) do
= link_to tags_project_repository_path(@project) do
Tags
- %span.badge= @project.repo.tag_count
+ %span.badge= @project.tags.length
- - if current_page?(project_commits_path(@project)) && current_user.private_token
+ - if current_controller?(:commits) && current_user.private_token
%li.right
%span.rss-icon
- = link_to project_commits_path(@project, :atom, { private_token: current_user.private_token, ref: @ref }), title: "Feed" do
+ = link_to project_commits_path(@project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed" do
= image_tag "rss_ui.png", title: "feed"
diff --git a/app/views/commits/_text_file.html.haml b/app/views/commits/_text_file.html.haml
index 9f5b5345..02117386 100644
--- a/app/views/commits/_text_file.html.haml
+++ b/app/views/commits/_text_file.html.haml
@@ -4,7 +4,7 @@
%table{class: "#{'hide' if too_big}"}
- each_diff_line(diff.diff.lines.to_a, index) do |line, type, line_code, line_new, line_old|
- %tr.line_holder
+ %tr.line_holder{ id: line_code }
- if type == "match"
%td.old_line= "..."
%td.new_line= "..."
diff --git a/app/views/commits/index.html.haml b/app/views/commits/index.html.haml
deleted file mode 100644
index 11ffdb6a..00000000
--- a/app/views/commits/index.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-= render "head"
-
-- if params[:path]
- %ul.breadcrumb
- %li
- %span.arrow
- = link_to project_commits_path(@project) do
- = @project.name
- %span.divider
- \/
- %li
- %a{href: "#"}= params[:path].split("/").join(" / ")
-
-%div{id: dom_id(@project)}
- #commits_list= render "commits"
-.clear
-.loading{ style: "display:none;"}
-
-- if @commits.count == @limit
- :javascript
- $(function(){
- CommitsList.init("#{@ref}", #{@limit});
- });
-
diff --git a/app/views/commits/index.atom.builder b/app/views/commits/show.atom.builder
similarity index 80%
rename from app/views/commits/index.atom.builder
rename to app/views/commits/show.atom.builder
index cca70456..46f9838e 100644
--- a/app/views/commits/index.atom.builder
+++ b/app/views/commits/show.atom.builder
@@ -1,8 +1,8 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "Recent commits to #{@project.name}:#{@ref}"
- xml.link :href => project_commits_url(@project, :atom, :ref => @ref), :rel => "self", :type => "application/atom+xml"
- xml.link :href => project_commits_url(@project), :rel => "alternate", :type => "text/html"
+ xml.link :href => project_commits_url(@project, @ref, format: :atom), :rel => "self", :type => "application/atom+xml"
+ xml.link :href => project_commits_url(@project, @ref), :rel => "alternate", :type => "text/html"
xml.id project_commits_url(@project)
xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any?
diff --git a/app/views/commits/show.html.haml b/app/views/commits/show.html.haml
index d12fff96..ac063638 100644
--- a/app/views/commits/show.html.haml
+++ b/app/views/commits/show.html.haml
@@ -1,10 +1,24 @@
-= render "commits/commit_box"
-= render "commits/diffs", diffs: @commit.diffs
-= render "notes/notes_with_form", tid: @commit.id, tt: "commit"
-= render "notes/per_line_form"
+= render "head"
+- if @path.present?
+ %ul.breadcrumb
+ %li
+ %span.arrow
+ = link_to project_commits_path(@project) do
+ = @project.name
+ %span.divider
+ \/
+ %li
+ %a{href: "#"}= @path.split("/").join(" / ")
+
+%div{id: dom_id(@project)}
+ #commits_list= render "commits"
+.clear
+.loading{ style: "display:none;"}
+
+- if @commits.count == @limit
+ :javascript
+ $(function(){
+ CommitsList.init("#{@ref}", #{@limit});
+ });
-:javascript
- $(function(){
- PerLineNotes.init();
- });
diff --git a/app/views/commits/index.js.haml b/app/views/commits/show.js.haml
similarity index 100%
rename from app/views/commits/index.js.haml
rename to app/views/commits/show.js.haml
diff --git a/app/views/commits/compare.html.haml b/app/views/compare/_form.html.haml
similarity index 64%
rename from app/views/commits/compare.html.haml
rename to app/views/compare/_form.html.haml
index db15ba53..07f1c818 100644
--- a/app/views/commits/compare.html.haml
+++ b/app/views/compare/_form.html.haml
@@ -1,9 +1,3 @@
-= render "head"
-
-%h3.page_title
- Compare View
-%hr
-
%div
%p.slead
Fill input field with commit id like
@@ -14,7 +8,7 @@
%br
- = form_tag compare_project_commits_path(@project), method: :get do
+ = form_tag project_compare_index_path(@project), method: :post do
.clearfix
= text_field_tag :from, params[:from], placeholder: "master", class: "xlarge"
= "..."
@@ -25,29 +19,14 @@
.actions
= submit_tag "Compare", class: "btn primary wide commits-compare-btn"
-- if @commits.present?
- %div.ui-box
- %h5.small Commits (#{@commits.count})
- %ul.unstyled= render @commits
-
- - unless @diffs.empty?
- %h4 Diff
- = render "commits/diffs", diffs: @diffs
-
:javascript
$(function() {
var availableTags = #{@project.ref_names.to_json};
- $("#from").autocomplete({
- source: availableTags,
- minLength: 1
- });
-
- $("#to").autocomplete({
+ $("#from, #to").autocomplete({
source: availableTags,
minLength: 1
});
disableButtonIfEmptyField('#to', '.commits-compare-btn');
});
-
diff --git a/app/views/compare/index.html.haml b/app/views/compare/index.html.haml
new file mode 100644
index 00000000..6c9a5fd8
--- /dev/null
+++ b/app/views/compare/index.html.haml
@@ -0,0 +1,7 @@
+= render "commits/head"
+
+%h3.page_title
+ Compare View
+%hr
+
+= render "form"
diff --git a/app/views/compare/show.html.haml b/app/views/compare/show.html.haml
new file mode 100644
index 00000000..528c8b44
--- /dev/null
+++ b/app/views/compare/show.html.haml
@@ -0,0 +1,16 @@
+= render "commits/head"
+
+%h3.page_title
+ Compare View
+%hr
+
+= render "form"
+
+- if @commits.present?
+ %div.ui-box
+ %h5.small Commits (#{@commits.count})
+ %ul.unstyled= render @commits
+
+ - unless @diffs.empty?
+ %h4 Diff
+ = render "commits/diffs", diffs: @diffs
diff --git a/app/views/dashboard/_groups.html.haml b/app/views/dashboard/_groups.html.haml
new file mode 100644
index 00000000..7c5e9f3f
--- /dev/null
+++ b/app/views/dashboard/_groups.html.haml
@@ -0,0 +1,20 @@
+.groups_box
+ %h5
+ Groups
+ %small
+ (#{groups.count})
+ - if current_user.can_create_group?
+ %span.right
+ = link_to new_admin_group_path, class: "btn very_small info" do
+ %i.icon-plus
+ New Group
+ %ul.unstyled
+ - groups.each do |group|
+ %li.wll
+ = link_to group_path(id: group.code), class: dom_class(group) do
+ %strong.group_name= truncate(group.name, length: 25)
+ %span.arrow
+ →
+ %span.last_activity
+ %strong Projects:
+ %span= group.projects.count
diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml
new file mode 100644
index 00000000..00f19ccd
--- /dev/null
+++ b/app/views/dashboard/_projects.html.haml
@@ -0,0 +1,21 @@
+.projects_box
+ %h5
+ Projects
+ %small
+ (#{projects.total_count})
+ - if current_user.can_create_project?
+ %span.right
+ = link_to new_project_path, class: "btn very_small info" do
+ %i.icon-plus
+ New Project
+ %ul.unstyled
+ - projects.each do |project|
+ %li.wll
+ = link_to project_path(project), class: dom_class(project) do
+ %strong.project_name= truncate(project.name, length: 25)
+ %span.arrow
+ →
+ %span.last_activity
+ %strong Last activity:
+ %span= project_last_activity(project)
+ .bottom= paginate projects, theme: "gitlab"
diff --git a/app/views/dashboard/index.atom.builder b/app/views/dashboard/index.atom.builder
index fa3bfade..ffa15258 100644
--- a/app/views/dashboard/index.atom.builder
+++ b/app/views/dashboard/index.atom.builder
@@ -12,6 +12,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.entry do
event_link = event.feed_url
event_title = event.feed_title
+ event_summary = event.feed_summary
xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
xml.link :href => event_link
@@ -22,7 +23,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.name event.author_name
xml.email event.author_email
end
- xml.summary event_title
+ xml.summary(:type => "xhtml") { |x| x << event_summary unless event_summary.nil? }
end
end
end
diff --git a/app/views/dashboard/index.html.haml b/app/views/dashboard/index.html.haml
index 791c18e3..dc520a22 100644
--- a/app/views/dashboard/index.html.haml
+++ b/app/views/dashboard/index.html.haml
@@ -9,28 +9,9 @@
.loading.hide
.side
= render "events/event_last_push", event: @last_push
- .projects_box
- %h5
- Projects
- %small
- (#{@projects.total_count})
- - if current_user.can_create_project?
- %span.right
- = link_to new_project_path, class: "btn very_small info" do
- %i.icon-plus
- New Project
- %ul.unstyled
- - @projects.each do |project|
- %li.wll
- = link_to project_path(project), class: dom_class(project) do
- %strong.project_name= truncate(project.name, length: 25)
- %span.arrow
- →
- %span.last_activity
- %strong Last activity:
- %span= project_last_activity(project)
- .bottom= paginate @projects, theme: "gitlab"
-
+ - if @groups.present?
+ = render "groups", groups: @groups
+ = render "projects", projects: @projects
%div
%span.rss-icon
= link_to dashboard_path(:atom, { private_token: current_user.private_token }) do
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index cb25d831..ea417aa9 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -1,8 +1,8 @@
- commit = CommitDecorator.decorate(commit)
%li.commit
%p
- = link_to commit.short_id(8), project_commit_path(project, id: commit.id), class: "commit_short_id"
- %strong.cdark= commit.author_name
+ = link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id"
+ %span= commit.author_name
–
= image_tag gravatar_icon(commit.author_email), class: "avatar", width: 16
= gfm escape_once(truncate(commit.title, length: 50)) rescue "--broken encoding"
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 7bae8db1..0d91a67a 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,17 +1,15 @@
- if event.allowed?
- - if event.issue?
- .event_feed
- = render "events/event_issue", event: event
+ %div.event-item
+ = event_image(event)
+ = image_tag gravatar_icon(event.author_email), class: "avatar"
- - elsif event.merge_request?
- .event_feed
- = render "events/event_merge_request", event: event
-
- - elsif event.push?
- .event_feed
- = render "events/event_push", event: event
-
- - elsif event.membership_changed?
- .event_feed
- = render "events/event_membership_changed", event: event
+ - if event.push?
+ = render "events/event/push", event: event
+ - else
+ = render "events/event/common", event: event
+ .clearfix
+ %span.cgray.right
+ = time_ago_in_words(event.created_at)
+ ago.
+ .clearfix
diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml
new file mode 100644
index 00000000..64dc02e3
--- /dev/null
+++ b/app/views/events/_event_issue.atom.haml
@@ -0,0 +1,2 @@
+%div{:xmlns => "http://www.w3.org/1999/xhtml"}
+ %p= simple_format issue.description
diff --git a/app/views/events/_event_issue.html.haml b/app/views/events/_event_issue.html.haml
deleted file mode 100644
index 4d357b7f..00000000
--- a/app/views/events/_event_issue.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-= image_tag gravatar_icon(event.author_email), class: "avatar"
-%strong #{event.author_name}
-%span.event_label{class: event.action_name}= event.action_name
-issue
-= link_to project_issue_path(event.project, event.issue) do
- %strong= truncate event.issue_title
-at
-%strong= link_to event.project.name, event.project
-%span.cgray
- = time_ago_in_words(event.created_at)
- ago.
diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
index 81b9994c..d70be70c 100644
--- a/app/views/events/_event_last_push.html.haml
+++ b/app/views/events/_event_last_push.html.haml
@@ -4,7 +4,7 @@
= image_tag gravatar_icon(event.author_email), class: "avatar"
%span You pushed to
= event.ref_type
- = link_to project_commits_path(event.project, ref: event.ref_name) do
+ = link_to project_commits_path(event.project, event.ref_name) do
%strong= truncate(event.ref_name, length: 28)
at
%strong= link_to event.project.name, event.project
diff --git a/app/views/events/_event_membership_changed.html.haml b/app/views/events/_event_membership_changed.html.haml
deleted file mode 100644
index 464f24b3..00000000
--- a/app/views/events/_event_membership_changed.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-= image_tag gravatar_icon(event.author_email), class: "avatar"
-%strong #{event.author_name}
-%span.event_label{class: event.action_name}= event.action_name
-project
-%strong= link_to event.project_name, event.project
-%span.cgray
- = time_ago_in_words(event.created_at)
- ago.
-
diff --git a/app/views/events/_event_merge_request.html.haml b/app/views/events/_event_merge_request.html.haml
deleted file mode 100644
index ceb39371..00000000
--- a/app/views/events/_event_merge_request.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- if event.action_name == "merged"
- .event_icon= image_tag "event_mr_merged.png"
-= image_tag gravatar_icon(event.author_email), class: "avatar"
-%strong #{event.author_name}
-%span.event_label{class: event.action_name}= event.action_name
-merge request
-= link_to project_merge_request_path(event.project, event.merge_request) do
- %strong= truncate event.merge_request_title
-at
-%strong= link_to event.project.name, event.project
-%span.cgray
- = time_ago_in_words(event.created_at)
- ago.
-%br
-%span= event.merge_request.source_branch
-→
-%span= event.merge_request.target_branch
-
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
new file mode 100644
index 00000000..d09e6e03
--- /dev/null
+++ b/app/views/events/_event_push.atom.haml
@@ -0,0 +1,14 @@
+%div{:xmlns => "http://www.w3.org/1999/xhtml"}
+ - event.commits.first(15).each do |commit|
+ %p
+ %strong= commit.author_name
+ = link_to "(##{commit.short_id})", project_commit_path(event.project, :id => commit.id)
+ %i
+ at
+ = commit.committed_date.strftime("%Y-%m-%d %H:%M:%S")
+ %blockquote= simple_format(escape_once(commit.safe_message))
+ - if event.commits_count > 15
+ %p
+ %i
+ \... and
+ = pluralize(event.commits_count - 15, "more commit")
diff --git a/app/views/events/_event_push.html.haml b/app/views/events/_event_push.html.haml
deleted file mode 100644
index 0adcaf9d..00000000
--- a/app/views/events/_event_push.html.haml
+++ /dev/null
@@ -1,30 +0,0 @@
-%div
- .event_icon= image_tag "event_push.png"
- = image_tag gravatar_icon(event.author_email), class: "avatar"
- %strong #{event.author_name}
- %span.event_label.pushed= event.push_action_name
- = event.ref_type
- = link_to project_commits_path(event.project, ref: event.ref_name) do
- %strong= event.ref_name
- at
- %strong= link_to event.project.name, event.project
- %span.cgray
- = time_ago_in_words(event.created_at)
- ago.
-
- - if event.push_with_commits?
- - if event.commits_count > 1
- = link_to compare_project_commits_path(event.project, from: event.parent_commit.id, to: event.last_commit.id) do
- %strong #{event.parent_commit.id[0..7]}...#{event.last_commit.id[0..7]}
- - project = event.project
- %ul.unstyled.event_commits
- - if event.commits_count > 3
- - event.commits[0...2].each do |commit|
- = render "events/commit", commit: commit, project: project
- %li
- %br
- \... and #{event.commits_count - 2} more commits
- - else
- - event.commits.each do |commit|
- = render "events/commit", commit: commit, project: project
-
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
new file mode 100644
index 00000000..dcabd1a9
--- /dev/null
+++ b/app/views/events/event/_common.html.haml
@@ -0,0 +1,13 @@
+.event-title
+ %span.author_name= link_to_author event
+ %span.event_label{class: event.action_name}= event_action_name(event)
+ - if event.target
+ = link_to [event.project, event.target] do
+ %strong= truncate event.target_title
+ - else
+ %strong= truncate event.target_title
+ at
+ - if event.project
+ = link_to_project event.project
+ - else
+ = event.project_name
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
new file mode 100644
index 00000000..869321ed
--- /dev/null
+++ b/app/views/events/event/_push.html.haml
@@ -0,0 +1,25 @@
+.event-title
+ %span.author_name= link_to_author event
+ %span.event_label.pushed #{event.push_action_name} #{event.ref_type}
+ - if event.rm_ref?
+ %strong= event.ref_name
+ - else
+ = link_to project_commits_path(event.project, event.ref_name) do
+ %strong= event.ref_name
+ at
+ %strong= link_to event.project.name, event.project
+
+- if event.push_with_commits?
+ - project = event.project
+ .event-body
+ %ul.unstyled.event_commits
+ - few_commits = event.commits[0...2]
+ - few_commits.each do |commit|
+ = render "events/commit", commit: commit, project: project
+
+ - if event.commits_count > 1
+ %li.commits-stat
+ - if event.commits_count > 2
+ %span ... and #{event.commits_count - 2} more commits.
+ = link_to project_compare_path(event.project, from: event.parent_commit.id, to: event.last_commit.id) do
+ %strong Compare → #{event.parent_commit.id[0..7]}...#{event.last_commit.id[0..7]}
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
new file mode 100644
index 00000000..b565dad3
--- /dev/null
+++ b/app/views/groups/_projects.html.haml
@@ -0,0 +1,15 @@
+.projects_box
+ %h5
+ Projects
+ %small
+ (#{projects.count})
+ %ul.unstyled
+ - projects.each do |project|
+ %li.wll
+ = link_to project_path(project), class: dom_class(project) do
+ %strong.project_name= truncate(project.name, length: 25)
+ %span.arrow
+ →
+ %span.last_activity
+ %strong Last activity:
+ %span= project_last_activity(project)
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
new file mode 100644
index 00000000..5bd07bcd
--- /dev/null
+++ b/app/views/groups/issues.atom.builder
@@ -0,0 +1,24 @@
+xml.instruct!
+xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
+ xml.title "#{@user.name} issues"
+ xml.link :href => dashboard_issues_url(:atom, :private_token => @user.private_token), :rel => "self", :type => "application/atom+xml"
+ xml.link :href => dashboard_issues_url(:private_token => @user.private_token), :rel => "alternate", :type => "text/html"
+ xml.id dashboard_issues_url(:private_token => @user.private_token)
+ xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+
+ @issues.each do |issue|
+ xml.entry do
+ xml.id project_issue_url(issue.project, issue)
+ xml.link :href => project_issue_url(issue.project, issue)
+ xml.title truncate(issue.title, :length => 80)
+ xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.media :thumbnail, :width => "40", :height => "40", :url => gravatar_icon(issue.author_email)
+ xml.author do |author|
+ xml.name issue.author_name
+ xml.email issue.author_email
+ end
+ xml.summary issue.title
+ end
+ end
+end
+
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
new file mode 100644
index 00000000..cc488d57
--- /dev/null
+++ b/app/views/groups/issues.html.haml
@@ -0,0 +1,19 @@
+%h3.page_title
+ Issues
+ %small (assigned to you)
+ %small.right #{@issues.total_count} issues
+
+%br
+.clearfix
+- if @issues.any?
+ - @issues.group_by(&:project).each do |group|
+ %div.ui-box
+ - @project = group[0]
+ %h5= @project.name
+ %ul.unstyled.issues_table
+ - group[1].each do |issue|
+ = render(partial: 'issues/show', locals: {issue: issue})
+ %hr
+ = paginate @issues, theme: "gitlab"
+- else
+ %h3.nothing_here_message Nothing to show here
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
new file mode 100644
index 00000000..23a7e722
--- /dev/null
+++ b/app/views/groups/merge_requests.html.haml
@@ -0,0 +1,18 @@
+%h3.page_title
+ Merge Requests
+ %small (authored by or assigned to you)
+ %small.right #{@merge_requests.total_count} merge requests
+
+%br
+- if @merge_requests.any?
+ - @merge_requests.group_by(&:project).each do |group|
+ %ul.unstyled.ui-box
+ - @project = group[0]
+ %h5= @project.name
+ - group[1].each do |merge_request|
+ = render(partial: 'merge_requests/merge_request', locals: {merge_request: merge_request})
+ %hr
+ = paginate @merge_requests, theme: "gitlab"
+
+- else
+ %h3.nothing_here_message Nothing to show here
diff --git a/app/views/groups/people.html.haml b/app/views/groups/people.html.haml
new file mode 100644
index 00000000..25810808
--- /dev/null
+++ b/app/views/groups/people.html.haml
@@ -0,0 +1,12 @@
+.ui-box
+ %h5
+ People
+ %small
+ (#{@users.size})
+ %ul.unstyled
+ - @users.each do |user|
+ %li.wll
+ = image_tag gravatar_icon(user.email, 16), class: "avatar s16"
+ %strong= user.name
+ %span.cgray= user.email
+
diff --git a/app/views/groups/search.html.haml b/app/views/groups/search.html.haml
new file mode 100644
index 00000000..6ca5630f
--- /dev/null
+++ b/app/views/groups/search.html.haml
@@ -0,0 +1,75 @@
+= form_tag search_group_path(@group), method: :get, class: 'form-inline' do |f|
+ .padded
+ = label_tag :search do
+ %strong Looking for
+ .input
+ = search_field_tag :search, params[:search], placeholder: "issue 143", class: "input-xxlarge search-text-input", id: "dashboard_search"
+ = submit_tag 'Search', class: "btn primary wide"
+- if params[:search].present?
+ %br
+ %h3
+ Search results
+ %small (#{@projects.count + @merge_requests.count + @issues.count})
+ %hr
+ .search_results
+ .row
+ .span6
+ %table
+ %thead
+ %tr
+ %th Projects
+ %tbody
+ - @projects.each do |project|
+ %tr
+ %td
+ = link_to project do
+ %strong.term= project.name
+ %small.cgray
+ last activity at
+ = project.last_activity_date.stamp("Aug 25, 2011")
+ - if @projects.blank?
+ %tr
+ %td
+ %h4.nothing_here_message No Projects
+ %br
+ %table
+ %thead
+ %tr
+ %th Merge Requests
+ %tbody
+ - @merge_requests.each do |merge_request|
+ %tr
+ %td
+ = link_to [merge_request.project, merge_request] do
+ %span.badge.badge-info ##{merge_request.id}
+ –
+ %strong.term= truncate merge_request.title, length: 50
+ %strong.right
+ %span.label= merge_request.project.name
+ - if @merge_requests.blank?
+ %tr
+ %td
+ %h4.nothing_here_message No Merge Requests
+ .span6
+ %table
+ %thead
+ %tr
+ %th Issues
+ %tbody
+ - @issues.each do |issue|
+ %tr
+ %td
+ = link_to [issue.project, issue] do
+ %span.badge.badge-info ##{issue.id}
+ –
+ %strong.term= truncate issue.title, length: 40
+ %strong.right
+ %span.label= issue.project.name
+ - if @issues.blank?
+ %tr
+ %td
+ %h4.nothing_here_message No Issues
+ :javascript
+ $(function() {
+ $(".search_results .term").highlight("#{params[:search]}");
+ })
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
new file mode 100644
index 00000000..fa3bfade
--- /dev/null
+++ b/app/views/groups/show.atom.builder
@@ -0,0 +1,29 @@
+xml.instruct!
+xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
+ xml.title "Dashboard feed#{" - #{current_user.name}" if current_user.name.present?}"
+ xml.link :href => projects_url(:atom), :rel => "self", :type => "application/atom+xml"
+ xml.link :href => projects_url, :rel => "alternate", :type => "text/html"
+ xml.id projects_url
+ xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+
+ @events.each do |event|
+ if event.allowed?
+ event = EventDecorator.decorate(event)
+ xml.entry do
+ event_link = event.feed_url
+ event_title = event.feed_title
+
+ xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
+ xml.link :href => event_link
+ xml.title truncate(event_title, :length => 80)
+ xml.updated event.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.media :thumbnail, :width => "40", :height => "40", :url => gravatar_icon(event.author_email)
+ xml.author do |author|
+ xml.name event.author_name
+ xml.email event.author_email
+ end
+ xml.summary event_title
+ end
+ end
+ end
+end
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
new file mode 100644
index 00000000..cd86a01f
--- /dev/null
+++ b/app/views/groups/show.html.haml
@@ -0,0 +1,30 @@
+.projects
+ .activities.span8
+ = link_to dashboard_path, class: 'btn very_small' do
+ ← To dashboard
+
+ %span.cgray Events and projects are filtered in scope of group
+ %hr
+ = render 'shared/no_ssh'
+ - if @events.any?
+ .content_list= render @events
+ - else
+ %h4.nothing_here_message Projects activity will be displayed here
+ .loading.hide
+ .side
+ = render "events/event_last_push", event: @last_push
+ = render "projects", projects: @projects
+ %div
+ %span.rss-icon
+ = link_to dashboard_path(:atom, { private_token: current_user.private_token }) do
+ = image_tag "rss_ui.png", title: "feed"
+ %strong News Feed
+
+ %hr
+ .gitlab-promo
+ = link_to "Homepage", "http://gitlabhq.com"
+ = link_to "Blog", "http://blog.gitlabhq.com"
+ = link_to "@gitlabhq", "https://twitter.com/gitlabhq"
+
+:javascript
+ $(function(){ Pager.init(20); });
diff --git a/app/views/groups/show.js.haml b/app/views/groups/show.js.haml
new file mode 100644
index 00000000..7e5a148e
--- /dev/null
+++ b/app/views/groups/show.js.haml
@@ -0,0 +1,2 @@
+:plain
+ Pager.append(#{@events.count}, "#{escape_javascript(render(@events))}");
diff --git a/app/views/issues/_form.html.haml b/app/views/issues/_form.html.haml
index 813ecab2..ec3edce0 100644
--- a/app/views/issues/_form.html.haml
+++ b/app/views/issues/_form.html.haml
@@ -12,7 +12,7 @@
= f.label :title do
%strong= "Subject *"
.input
- = f.text_field :title, maxlength: 255, class: "xxlarge"
+ = f.text_field :title, maxlength: 255, class: "xxlarge gfm-input"
.issue_middle_block
.issue_assignee
= f.label :assignee_id do
@@ -37,7 +37,7 @@
.clearfix
= f.label :description, "Details"
.input
- = f.text_area :description, maxlength: 2000, class: "xxlarge", rows: 14
+ = f.text_area :description, maxlength: 2000, class: "xxlarge gfm-input", rows: 14
%p.hint Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
diff --git a/app/views/issues/_head.html.haml b/app/views/issues/_head.html.haml
index 8ebe3e05..4294503c 100644
--- a/app/views/issues/_head.html.haml
+++ b/app/views/issues/_head.html.haml
@@ -1,13 +1,10 @@
%ul.nav.nav-tabs
- %li{class: "#{'active' if current_page?(project_issues_path(@project))}"}
- = link_to project_issues_path(@project), class: "tab" do
- Browse Issues
- %li{class: "#{'active' if current_page?(project_milestones_path(@project))}"}
- = link_to project_milestones_path(@project), class: "tab" do
- Milestones
- %li{class: "#{'active' if current_page?(project_labels_path(@project))}"}
- = link_to project_labels_path(@project), class: "tab" do
- Labels
+ = nav_link(controller: :issues) do
+ = link_to 'Browse Issues', project_issues_path(@project), class: "tab"
+ = nav_link(controller: :milestones) do
+ = link_to 'Milestones', project_milestones_path(@project), class: "tab"
+ = nav_link(controller: :labels) do
+ = link_to 'Labels', project_labels_path(@project), class: "tab"
%li.right
%span.rss-icon
= link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }) do
diff --git a/app/views/issues/_show.html.haml b/app/views/issues/_show.html.haml
index 64401bdd..4b6823f5 100644
--- a/app/views/issues/_show.html.haml
+++ b/app/views/issues/_show.html.haml
@@ -15,7 +15,7 @@
- if issue.closed
= link_to 'Reopen', project_issue_path(issue.project, issue, issue: {closed: false }, status_only: true), method: :put, class: "btn small grouped reopen_issue", remote: true
- else
- = link_to 'Resolve', project_issue_path(issue.project, issue, issue: {closed: true }, status_only: true), method: :put, class: "success btn small grouped close_issue", remote: true
+ = link_to 'Close', project_issue_path(issue.project, issue, issue: {closed: true }, status_only: true), method: :put, class: "btn small grouped close_issue", remote: true
= link_to edit_project_issue_path(issue.project, issue), class: "btn small edit-issue-link", remote: true do
%i.icon-edit
Edit
diff --git a/app/views/issues/show.html.haml b/app/views/issues/show.html.haml
index da2aeac4..9114febd 100644
--- a/app/views/issues/show.html.haml
+++ b/app/views/issues/show.html.haml
@@ -8,9 +8,9 @@
%span.right
- if can?(current_user, :admin_project, @project) || @issue.author == current_user
- if @issue.closed
- = link_to 'Reopen', project_issue_path(@project, @issue, issue: {closed: false }, status_only: true), method: :put, class: "btn grouped success"
+ = link_to 'Reopen', project_issue_path(@project, @issue, issue: {closed: false }, status_only: true), method: :put, class: "btn grouped reopen_issue"
- else
- = link_to 'Close', project_issue_path(@project, @issue, issue: {closed: true }, status_only: true), method: :put, class: "btn grouped danger", title: "Close Issue"
+ = link_to 'Close', project_issue_path(@project, @issue, issue: {closed: true }, status_only: true), method: :put, class: "btn grouped close_issue", title: "Close Issue"
- if can?(current_user, :admin_project, @project) || @issue.author == current_user
= link_to edit_project_issue_path(@project, @issue), class: "btn grouped" do
%i.icon-edit
diff --git a/app/views/layouts/_app_menu.html.haml b/app/views/layouts/_app_menu.html.haml
deleted file mode 100644
index 02531489..00000000
--- a/app/views/layouts/_app_menu.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-%ul.main_menu
- %li.home{class: tab_class(:root)}
- = link_to "Home", root_path, title: "Home"
-
- %li{class: tab_class(:dash_issues)}
- = link_to dashboard_issues_path do
- Issues
- %span.count= current_user.assigned_issues.opened.count
-
- %li{class: tab_class(:dash_mr)}
- = link_to dashboard_merge_requests_path do
- Merge Requests
- %span.count= current_user.cared_merge_requests.count
-
- %li{class: tab_class(:search)}
- = link_to "Search", search_path
-
- %li{class: tab_class(:help)}
- = link_to "Help", help_path
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index c076a3a1..25fe9d80 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -10,8 +10,8 @@
- if controller_name == 'projects' && action_name == 'index'
= auto_discovery_link_tag :atom, projects_url(:atom, private_token: current_user.private_token), title: "Dashboard feed"
- if @project && !@project.new_record?
- - if current_page?(tree_project_ref_path(@project, @project.root_ref)) || current_page?(project_commits_path(@project))
- = auto_discovery_link_tag(:atom, project_commits_url(@project, :atom, ref: @ref, private_token: current_user.private_token), title: "Recent commits to #{@project.name}:#{@ref}")
- - if request.path == project_issues_path(@project)
+ - if current_controller?(:tree, :commits)
+ = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, format: :atom, private_token: current_user.private_token), title: "Recent commits to #{@project.name}:#{@ref}")
+ - if current_controller?(:issues)
= auto_discovery_link_tag(:atom, project_issues_url(@project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues")
= csrf_meta_tags
diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml
index f5e423a5..7f89bdf0 100644
--- a/app/views/layouts/_head_panel.html.haml
+++ b/app/views/layouts/_head_panel.html.haml
@@ -28,6 +28,8 @@
My profile
= link_to 'Logout', destroy_user_session_path, class: "logout", method: :delete
+= render "layouts/init_auto_complete"
+
:javascript
$(function(){
$("#search").autocomplete({
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
new file mode 100644
index 00000000..87a74655
--- /dev/null
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -0,0 +1,17 @@
+:javascript
+ $(function() {
+ autocompleteMembersUrl = "#{ "/api/v2/projects/#{@project.code}/members" if @project }";
+ autocompleteMembersParams.private_token = "#{current_user.authentication_token}";
+
+ autocompleteEmojiData = #{raw emoji_autocomplete_source};
+ // convert the list so that the items have the right format for completion
+ autocompleteEmojiData = $.map(autocompleteEmojiData, function(value) {
+ return {
+ name: value,
+ insert: value+':',
+ image: '#{image_path("emoji")}/'+value+'.png'
+ }
+ });
+
+ setupGfmAutoComplete();
+ });
diff --git a/app/views/layouts/_project_menu.html.haml b/app/views/layouts/_project_menu.html.haml
deleted file mode 100644
index 04eaec5a..00000000
--- a/app/views/layouts/_project_menu.html.haml
+++ /dev/null
@@ -1,37 +0,0 @@
-%ul.main_menu
- %li.home{class: project_tab_class}
- = link_to @project.code, project_path(@project), title: "Project"
-
- - if @project.repo_exists?
- - if can? current_user, :download_code, @project
- %li{class: tree_tab_class}
- = link_to tree_project_ref_path(@project, @project.root_ref) do
- Files
- %li{class: commit_tab_class}
- = link_to "Commits", project_commits_path(@project)
-
- %li{class: tab_class(:network)}
- = link_to "Network", graph_project_path(@project)
-
- - if @project.issues_enabled
- %li{class: tab_class(:issues)}
- = link_to project_issues_filter_path(@project) do
- Issues
- %span.count.issue_counter= @project.issues.opened.count
-
- - if @project.repo_exists?
- - if @project.merge_requests_enabled
- %li{class: tab_class(:merge_requests)}
- = link_to project_merge_requests_path(@project) do
- Merge Requests
- %span.count.merge_counter= @project.merge_requests.opened.count
-
- - if @project.wall_enabled
- %li{class: tab_class(:wall)}
- = link_to wall_project_path(@project) do
- Wall
-
- - if @project.wiki_enabled
- %li{class: tab_class(:wiki)}
- = link_to project_wiki_path(@project, :index) do
- Wiki
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index 6af0f641..582f86ba 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -6,17 +6,17 @@
= render "layouts/head_panel", title: "Admin area"
.container
%ul.main_menu
- %li.home{class: tab_class(:admin_root)}
+ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
= link_to "Stats", admin_root_path
- %li{class: tab_class(:admin_projects)}
+ = nav_link(controller: [:projects, :groups]) do
= link_to "Projects", admin_projects_path
- %li{class: tab_class(:admin_users)}
+ = nav_link(controller: :users) do
= link_to "Users", admin_users_path
- %li{class: tab_class(:admin_logs)}
+ = nav_link(controller: :logs) do
= link_to "Logs", admin_logs_path
- %li{class: tab_class(:admin_hooks)}
+ = nav_link(controller: :hooks) do
= link_to "Hooks", admin_hooks_path
- %li{class: tab_class(:admin_resque)}
+ = nav_link(controller: :resque) do
= link_to "Resque", admin_resque_path
.content= yield
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index dda10d5d..40f4f88c 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -5,6 +5,20 @@
= render "layouts/flash"
= render "layouts/head_panel", title: "Dashboard"
.container
- = render partial: "layouts/app_menu"
- .content
- = yield
+ %ul.main_menu
+ = nav_link(path: 'dashboard#index', html_options: {class: 'home'}) do
+ = link_to "Home", root_path, title: "Home"
+ = nav_link(path: 'dashboard#issues') do
+ = link_to dashboard_issues_path do
+ Issues
+ %span.count= current_user.assigned_issues.opened.count
+ = nav_link(path: 'dashboard#merge_requests') do
+ = link_to dashboard_merge_requests_path do
+ Merge Requests
+ %span.count= current_user.cared_merge_requests.count
+ = nav_link(path: 'search#show') do
+ = link_to "Search", search_path
+ = nav_link(path: 'help#index') do
+ = link_to "Help", help_path
+
+ .content= yield
diff --git a/app/views/layouts/devise_layout.html.haml b/app/views/layouts/devise.html.haml
similarity index 100%
rename from app/views/layouts/devise_layout.html.haml
rename to app/views/layouts/devise.html.haml
diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/errors.html.haml
similarity index 100%
rename from app/views/layouts/error.html.haml
rename to app/views/layouts/errors.html.haml
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
new file mode 100644
index 00000000..985200e2
--- /dev/null
+++ b/app/views/layouts/group.html.haml
@@ -0,0 +1,24 @@
+!!! 5
+%html{ lang: "en"}
+ = render "layouts/head"
+ %body{class: "#{app_theme} application"}
+ = render "layouts/flash"
+ = render "layouts/head_panel", title: "#{@group.name}"
+ .container
+ %ul.main_menu
+ = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
+ = link_to "Home", group_path(@group), title: "Home"
+ = nav_link(path: 'groups#issues') do
+ = link_to issues_group_path(@group) do
+ Issues
+ %span.count= current_user.assigned_issues.opened.of_group(@group).count
+ = nav_link(path: 'groups#merge_requests') do
+ = link_to merge_requests_group_path(@group) do
+ Merge Requests
+ %span.count= current_user.cared_merge_requests.of_group(@group).count
+ = nav_link(path: 'groups#search') do
+ = link_to "Search", search_group_path(@group)
+ = nav_link(path: 'groups#people') do
+ = link_to "People", people_group_path(@group)
+
+ .content= yield
diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index 62c8db5b..7a54bb7c 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -6,23 +6,17 @@
= render "layouts/head_panel", title: "Profile"
.container
%ul.main_menu
- %li.home{class: tab_class(:profile)}
+ = nav_link(path: 'profile#show', html_options: {class: 'home'}) do
= link_to "Profile", profile_path
-
- %li{class: tab_class(:account)}
+ = nav_link(path: 'profile#account') do
= link_to "Account", profile_account_path
-
- %li{class: tab_class(:ssh_keys)}
+ = nav_link(controller: :keys) do
= link_to keys_path do
SSH Keys
%span.count= current_user.keys.count
-
- %li{class: tab_class(:design)}
+ = nav_link(path: 'profile#design') do
= link_to "Design", profile_design_path
-
- %li{class: tab_class(:history)}
+ = nav_link(path: 'profile#history') do
= link_to "History", profile_history_path
-
- .content
- = yield
+ .content= yield
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
deleted file mode 100644
index 56a947d2..00000000
--- a/app/views/layouts/project.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head"
- %body{class: "#{app_theme} project"}
- = render "layouts/flash"
- = render "layouts/head_panel", title: @project.name
- .container
- = render partial: "layouts/project_menu"
- .content
- = yield
-
diff --git a/app/views/layouts/project_resource.html.haml b/app/views/layouts/project_resource.html.haml
new file mode 100644
index 00000000..b1dbe41c
--- /dev/null
+++ b/app/views/layouts/project_resource.html.haml
@@ -0,0 +1,41 @@
+!!! 5
+%html{ lang: "en"}
+ = render "layouts/head"
+ %body{class: "#{app_theme} project"}
+ = render "layouts/flash"
+ = render "layouts/head_panel", title: @project.name
+ .container
+ %ul.main_menu
+ = nav_link(html_options: {class: "home #{project_tab_class}"}) do
+ = link_to @project.code, project_path(@project), title: "Project"
+
+ - if @project.repo_exists?
+ - if can? current_user, :download_code, @project
+ = nav_link(controller: %w(tree blob blame)) do
+ = link_to 'Files', project_tree_path(@project, @ref || @project.root_ref)
+ = nav_link(controller: %w(commit commits compare repositories protected_branches)) do
+ = link_to "Commits", project_commits_path(@project, @ref || @project.root_ref)
+ = nav_link(path: 'projects#graph') do
+ = link_to "Network", graph_project_path(@project)
+
+ - if @project.issues_enabled
+ = nav_link(controller: %w(issues milestones labels)) do
+ = link_to project_issues_filter_path(@project) do
+ Issues
+ %span.count.issue_counter= @project.issues.opened.count
+
+ - if @project.repo_exists? && @project.merge_requests_enabled
+ = nav_link(controller: :merge_requests) do
+ = link_to project_merge_requests_path(@project) do
+ Merge Requests
+ %span.count.merge_counter= @project.merge_requests.opened.count
+
+ - if @project.wall_enabled
+ = nav_link(path: 'projects#wall') do
+ = link_to 'Wall', wall_project_path(@project)
+
+ - if @project.wiki_enabled
+ = nav_link(controller: :wikis) do
+ = link_to 'Wiki', project_wiki_path(@project, :index)
+
+ .content= yield
diff --git a/app/views/merge_requests/_form.html.haml b/app/views/merge_requests/_form.html.haml
index 96692c0f..30f88102 100644
--- a/app/views/merge_requests/_form.html.haml
+++ b/app/views/merge_requests/_form.html.haml
@@ -12,11 +12,8 @@
.span5
.mr_branch_box
%h5 From (Head Branch)
- .body
- .padded
- = f.label :source_branch, "From", class: "control-label"
- .controls
- = f.select(:source_branch, @project.heads.map(&:name), { include_blank: "Select branch" }, {class: 'chosen span3'})
+ .body
+ .padded= f.select(:source_branch, @project.heads.map(&:name), { include_blank: "Select branch" }, {class: 'chosen span4'})
.mr_source_commit
.span2
@@ -25,10 +22,7 @@
.mr_branch_box
%h5 To (Base Branch)
.body
- .padded
- = f.label :target_branch, "To", class: "control-label"
- .controls
- = f.select(:target_branch, @project.heads.map(&:name), { include_blank: "Select branch" }, {class: 'chosen span3'})
+ .padded= f.select(:target_branch, @project.heads.map(&:name), { include_blank: "Select branch" }, {class: 'chosen span4'})
.mr_target_commit
%h4.cdark 2. Fill info
@@ -38,7 +32,7 @@
.top_box_content
= f.label :title do
%strong= "Title *"
- .input= f.text_field :title, class: "input-xxlarge pad", maxlength: 255, rows: 5
+ .input= f.text_field :title, class: "input-xxlarge pad gfm-input", maxlength: 255, rows: 5
.middle_box_content
= f.label :assignee_id do
%i.icon-user
diff --git a/app/views/merge_requests/_merge_request.html.haml b/app/views/merge_requests/_merge_request.html.haml
index 9d94d670..419419d2 100644
--- a/app/views/merge_requests/_merge_request.html.haml
+++ b/app/views/merge_requests/_merge_request.html.haml
@@ -9,7 +9,7 @@
- if merge_request.notes.any?
%span.btn.small.disabled.grouped
%i.icon-comment
- = merge_request.notes.count
+ = merge_request.mr_and_commit_notes.count
%span.btn.small.disabled.grouped
= merge_request.source_branch
→
diff --git a/app/views/milestones/show.html.haml b/app/views/milestones/show.html.haml
index ba71ead7..c113c81f 100644
--- a/app/views/milestones/show.html.haml
+++ b/app/views/milestones/show.html.haml
@@ -1,9 +1,12 @@
-%h3
+%h3.page_title
Milestone ##{@milestone.id}
%small
= @milestone.expires_at
%span.right
+ = link_to new_project_issue_path(@project, issue: { milestone_id: @milestone.id }), class: "btn small grouped", title: "New Issue" do
+ %i.icon-plus
+ New Issue
= link_to 'Browse Issues', project_issues_path(@milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link small grouped"
- if can?(current_user, :admin_milestone, @project)
= link_to edit_project_milestone_path(@project, @milestone), class: "btn small grouped" do
@@ -28,9 +31,9 @@
%h5
Progress:
%small
- #{@milestone.issues.opened.count} open
- –
#{@milestone.issues.closed.count} closed
+ –
+ #{@milestone.issues.opened.count} open
.progress.progress-success
.bar{style: "width: #{@milestone.percent_complete}%;"}
@@ -42,18 +45,20 @@
.row
.span6
- %table
+ %table.milestone-issue-filter
%thead
- %th Open Issues
+ %th
+ %ul.nav.nav-pills
+ %li.active= link_to('Open Issues', '#')
+ %li=link_to('All Issues', '#')
- @issues.each do |issue|
- %tr
+ %tr{data: {closed: issue.closed}}
%td
= link_to [@project, issue] do
%span.badge.badge-info ##{issue.id}
–
= link_to_gfm truncate(issue.title, length: 60), [@project, issue]
%br
- = paginate @issues, theme: "gitlab"
.span6
%table
diff --git a/app/views/notes/_common_form.html.haml b/app/views/notes/_common_form.html.haml
index fc6e3c7e..a9f2907b 100644
--- a/app/views/notes/_common_form.html.haml
+++ b/app/views/notes/_common_form.html.haml
@@ -8,7 +8,7 @@
= f.hidden_field :noteable_id
= f.hidden_field :noteable_type
- = f.text_area :note, size: 255, class: 'note-text'
+ = f.text_area :note, size: 255, class: 'note-text gfm-input'
#preview-note.preview_note.hide
.hint
.right Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
@@ -36,4 +36,3 @@
%a.file_upload.btn.small Upload File
= f.file_field :attachment, class: "input-file"
%span.hint Any file less than 10 MB
-
diff --git a/app/views/notes/_create_common_note.js.haml b/app/views/notes/_create_common_note.js.haml
index bbebc247..e7df64c4 100644
--- a/app/views/notes/_create_common_note.js.haml
+++ b/app/views/notes/_create_common_note.js.haml
@@ -10,4 +10,5 @@
- else
:plain
$(".note-form-holder").replaceWith("#{escape_javascript(render 'form')}");
+ setupGfmAutoComplete();
diff --git a/app/views/notes/_note.html.haml b/app/views/notes/_note.html.haml
index 5234e55d..70baa212 100644
--- a/app/views/notes/_note.html.haml
+++ b/app/views/notes/_note.html.haml
@@ -1,4 +1,4 @@
-%li{id: dom_id(note), class: "note #{note_vote_class(note)}"}
+%li{id: dom_id(note), class: "note"}
= image_tag gravatar_icon(note.author.email), class: "avatar s32"
%div.note-author
%strong= note.author_name
@@ -6,14 +6,25 @@
%cite.cgray
= time_ago_in_words(note.updated_at)
ago
- - if note.upvote?
- %span.label.label-success
- %i.icon-thumbs-up
- \+1
- - if note.downvote?
- %span.label.label-error
- %i.icon-thumbs-down
- \-1
+
+ - unless note_for_main_target?(note)
+ - if note.for_commit?
+ %span.cgray
+ on #{link_to note.noteable.short_id, project_commit_path(@project, note.noteable)}
+ = link_to_commit_diff_line_note(note) if note.for_diff_line?
+
+ -# only show vote if it's a note for the main target
+ - if note_for_main_target?(note)
+ - if note.upvote?
+ %span.vote.upvote.label.label-success
+ %i.icon-thumbs-up
+ \+1
+ - if note.downvote?
+ %span.vote.downvote.label.label-error
+ %i.icon-thumbs-down
+ \-1
+
+ -# remove button
- if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project)
= link_to [@project, note], confirm: 'Are you sure?', method: :delete, remote: true, class: "cred delete-note btn very_small" do
%i.icon-trash
diff --git a/app/views/notes/_per_line_form.html.haml b/app/views/notes/_per_line_form.html.haml
index 8e31b59e..ee0cde4b 100644
--- a/app/views/notes/_per_line_form.html.haml
+++ b/app/views/notes/_per_line_form.html.haml
@@ -13,7 +13,7 @@
= f.hidden_field :noteable_id
= f.hidden_field :noteable_type
= f.hidden_field :line_code
- = f.text_area :note, size: 255, class: 'line-note-text'
+ = f.text_area :note, size: 255, class: 'line-note-text gfm-input'
.note_actions
.buttons
= f.submit 'Add note', class: "btn save-btn submit_note submit_inline_note", id: "submit_note"
diff --git a/app/views/notify/note_wiki_email.html.haml b/app/views/notify/note_wiki_email.html.haml
index d3840cda..41a237fc 100644
--- a/app/views/notify/note_wiki_email.html.haml
+++ b/app/views/notify/note_wiki_email.html.haml
@@ -5,7 +5,7 @@
%td{align: "left", style: "padding: 20px 0 0;"}
%h2{style: "color:#646464 !important; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "}
New comment for Wiki page
- = link_to_gfm @wiki.title, project_issue_url(@wiki.project, @wiki, anchor: "note_#{@note.id}")
+ = link_to_gfm @wiki.title, project_wiki_url(@wiki.project, @wiki, anchor: "note_#{@note.id}")
%td{style: "font-size: 1px; line-height: 1px;", width: "21"}
%tr
%td{style: "font-size: 1px; line-height: 1px;", width: "21"}
diff --git a/app/views/projects/_clone_panel.html.haml b/app/views/projects/_clone_panel.html.haml
index 20891610..4411ff17 100644
--- a/app/views/projects/_clone_panel.html.haml
+++ b/app/views/projects/_clone_panel.html.haml
@@ -4,7 +4,7 @@
.form-horizontal
.input-prepend.project_clone_holder
%button{class: "btn small active", :"data-clone" => @project.ssh_url_to_repo} SSH
- %button{class: "btn small", :"data-clone" => @project.http_url_to_repo} HTTP
+ %button{class: "btn small", :"data-clone" => @project.http_url_to_repo}= Gitlab.config.web_protocol.upcase
= text_field_tag :project_clone, @project.url_to_repo, class: "one_click_select span5"
.span4.right
.right
diff --git a/app/views/projects/_project_head.html.haml b/app/views/projects/_project_head.html.haml
index 4f38bef8..47a79073 100644
--- a/app/views/projects/_project_head.html.haml
+++ b/app/views/projects/_project_head.html.haml
@@ -1,29 +1,27 @@
%ul.nav.nav-tabs
- %li{ class: "#{'active' if current_page?(project_path(@project)) }" }
+ = nav_link(path: 'projects#show') do
= link_to project_path(@project), class: "activities-tab tab" do
%i.icon-home
Show
- %li{ class: " #{'active' if (controller.controller_name == "team_members") || current_page?(project_team_index_path(@project)) }" }
+ = nav_link(controller: :team_members) do
= link_to project_team_index_path(@project), class: "team-tab tab" do
%i.icon-user
Team
- %li{ class: "#{'active' if current_page?(files_project_path(@project)) }" }
- = link_to files_project_path(@project), class: "files-tab tab " do
- Attachments
- %li{ class: " #{'active' if (controller.controller_name == "snippets") }" }
- = link_to project_snippets_path(@project), class: "snippets-tab tab" do
- Snippets
+ = nav_link(path: 'projects#files') do
+ = link_to 'Attachments', files_project_path(@project), class: "files-tab tab"
+ = nav_link(controller: :snippets) do
+ = link_to 'Snippets', project_snippets_path(@project), class: "snippets-tab tab"
- if can? current_user, :admin_project, @project
- %li.right{class: "#{'active' if controller.controller_name == "deploy_keys"}"}
+ = nav_link(controller: :deploy_keys, html_options: {class: 'right'}) do
= link_to project_deploy_keys_path(@project) do
%span
Deploy Keys
- %li.right{class: "#{'active' if controller.controller_name == "hooks" }"}
+ = nav_link(controller: :hooks, html_options: {class: 'right'}) do
= link_to project_hooks_path(@project) do
%span
Hooks
- %li.right{ class: "#{'active' if current_page?(edit_project_path(@project)) }" }
+ = nav_link(path: 'projects#edit', html_options: {class: 'right'}) do
= link_to edit_project_path(@project), class: "stat-tab tab " do
%i.icon-edit
Edit
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 21459da2..2c4f55eb 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -2,3 +2,7 @@
= render 'clone_panel'
= render "events/event_last_push", event: @last_push
.content_list= render @events
+.loading.hide
+
+:javascript
+ $(function(){ Pager.init(20); });
diff --git a/app/views/projects/show.js.haml b/app/views/projects/show.js.haml
new file mode 100644
index 00000000..511f2789
--- /dev/null
+++ b/app/views/projects/show.js.haml
@@ -0,0 +1,2 @@
+:plain
+ Pager.append(#{@events.count}, "#{escape_javascript(render(@events))}");
diff --git a/app/views/protected_branches/index.html.haml b/app/views/protected_branches/index.html.haml
index 43884de1..50a81712 100644
--- a/app/views/protected_branches/index.html.haml
+++ b/app/views/protected_branches/index.html.haml
@@ -34,7 +34,7 @@
- @branches.each do |branch|
%tr
%td
- = link_to project_commits_path(@project, ref: branch.name) do
+ = link_to project_commits_path(@project, branch.name) do
%strong= branch.name
- if branch.name == @project.root_ref
%span.label default
diff --git a/app/views/refs/_tree.html.haml b/app/views/refs/_tree.html.haml
deleted file mode 100644
index 55078718..00000000
--- a/app/views/refs/_tree.html.haml
+++ /dev/null
@@ -1,70 +0,0 @@
-%ul.breadcrumb
- %li
- %span.arrow
- = link_to tree_project_ref_path(@project, @ref, path: nil), remote: true do
- = @project.name
- - tree.breadcrumbs(6) do |link|
- \/
- %li= link
-.clear
-%div.tree_progress
-#tree-content-holder
- - if tree.is_blob?
- = render partial: "refs/tree_file", locals: { name: tree.name, content: tree.data, file: tree }
- - else
- - contents = tree.contents
- %table#tree-slider{class: "table_#{@hex_path}" }
- %thead
- %th Name
- %th Last Update
- %th
- Last commit
- = link_to "History", tree.history_path, class: "right"
-
- - if tree.up_dir?
- %tr{ class: "tree-item", url: tree.up_dir_path }
- %td.tree-item-file-name
- = image_tag "file_empty.png"
- = link_to "..", tree.up_dir_path, remote: :true
- %td
- %td
-
- - index = 0
- - contents.select{ |i| i.is_a?(Grit::Tree)}.each do |content|
- = render partial: "refs/tree_item", locals: { content: content, index: (index += 1) }
- - contents.select{ |i| i.is_a?(Grit::Blob)}.each do |content|
- = render partial: "refs/tree_item", locals: { content: content, index: (index += 1) }
- - contents.select{ |i| i.is_a?(Grit::Submodule)}.each do |content|
- = render partial: "refs/submodule_item", locals: { content: content, index: (index += 1) }
-
- - if content = contents.select{ |c| c.is_a?(Grit::Blob) and c.name =~ /^readme/i }.first
- .file_holder#README
- .file_title
- %i.icon-file
- = content.name
- .file_content.wiki
- - if gitlab_markdown?(content.name)
- = preserve do
- = markdown(content.data)
- - else
- = raw GitHub::Markup.render(content.name, content.data)
-
-:javascript
- $(function(){
- history.pushState({ path: this.path }, '', "#{@history_path}");
- });
-
-- unless tree.is_blob?
- :javascript
- // Load last commit log for each file in tree
- $(window).load(function(){
- ajaxGet('#{@logs_path}');
- });
-
-- if params[:path] && request.xhr?
- :javascript
- $(window).unbind('popstate');
- $(window).bind('popstate', function() {
- if(location.pathname.search("tree") != -1) {
- $.ajax({type: "GET", url: location.pathname, dataType: "script"})}
- else { location.href = location.pathname;}});
diff --git a/app/views/refs/_tree_commit.html.haml b/app/views/refs/_tree_commit.html.haml
deleted file mode 100644
index 1bcf1a7e..00000000
--- a/app/views/refs/_tree_commit.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- if tm
- = link_to "[#{tm.user_name}]", project_team_member_path(@project, tm)
-= link_to_gfm truncate(content_commit.title, length: tm ? 30 : 50), project_commit_path(@project, content_commit.id), class: "tree-commit-link"
diff --git a/app/views/refs/_tree_file.html.haml b/app/views/refs/_tree_file.html.haml
deleted file mode 100644
index 76173e24..00000000
--- a/app/views/refs/_tree_file.html.haml
+++ /dev/null
@@ -1,40 +0,0 @@
-.file_holder
- .file_title
- %i.icon-file
- %span.file_name
- = name.force_encoding('utf-8')
- %small #{file.mode}
- %span.options
- = link_to "raw", blob_project_ref_path(@project, @ref, path: params[:path]), class: "btn very_small", target: "_blank"
- = link_to "history", project_commits_path(@project, path: params[:path], ref: @ref), class: "btn very_small"
- = link_to "blame", blame_file_project_ref_path(@project, @ref, path: params[:path]), class: "btn very_small"
- - if file.text?
- - if gitlab_markdown?(name)
- .file_content.wiki
- = preserve do
- = markdown(file.data)
- - elsif markup?(name)
- .file_content.wiki
- = raw GitHub::Markup.render(name, file.data)
- - else
- .file_content.code
- - unless file.empty?
- %div{class: current_user.dark_scheme ? "black" : "white"}
- = preserve do
- = raw file.colorize(options: { linenos: 'True'})
- - else
- %h4.nothing_here_message Empty file
-
- - elsif file.image?
- .file_content.image_file
- %img{ src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
-
- - else
- .file_content.blob_file
- %center
- = link_to blob_project_ref_path(@project, @ref, path: params[:path]) do
- %div.padded
- %br
- = image_tag "download.png", width: 64
- %h3
- Download (#{file.mb_size})
diff --git a/app/views/refs/_tree_item.html.haml b/app/views/refs/_tree_item.html.haml
deleted file mode 100644
index d4c4ee8d..00000000
--- a/app/views/refs/_tree_item.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- file = tree_full_path(content)
-%tr{ class: "tree-item #{tree_hex_class(content)}", url: tree_file_project_ref_path(@project, @ref, file) }
- %td.tree-item-file-name
- = tree_icon(content)
- %strong= link_to truncate(content.name, length: 40), tree_file_project_ref_path(@project, @ref || @commit.id, file), remote: :true
- %td.tree_time_ago.cgray
- - if index == 1
- %span.log_loading
- Loading commit data..
- = image_tag "ajax_loader_tree.gif", width: 14
- %td.tree_commit
diff --git a/app/views/refs/logs_tree.js.haml b/app/views/refs/logs_tree.js.haml
index 61ccbaee..23a6dae7 100644
--- a/app/views/refs/logs_tree.js.haml
+++ b/app/views/refs/logs_tree.js.haml
@@ -1,9 +1,8 @@
- @logs.each do |content_data|
- file_name = content_data[:file_name]
- - content_commit = content_data[:commit]
- - tm = @project.team_member_by_name_or_email(content_commit.author_email, content_commit.author_name)
+ - commit = content_data[:commit]
:plain
var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}");
- row.find("td.tree_time_ago").html('#{escape_javascript(time_ago_in_words(content_commit.committed_date))} ago');
- row.find("td.tree_commit").html('#{escape_javascript(render("tree_commit", tm: tm, content_commit: content_commit))}');
+ row.find("td.tree_time_ago").html('#{escape_javascript time_ago_in_words(commit.committed_date)} ago');
+ row.find("td.tree_commit").html('#{escape_javascript render("tree/tree_commit_column", commit: commit)}');
diff --git a/app/views/refs/tree.html.haml b/app/views/refs/tree.html.haml
deleted file mode 100644
index 181be642..00000000
--- a/app/views/refs/tree.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-= render "head"
-#tree-holder= render partial: "tree", locals: {repo: @repo, commit: @commit, tree: @tree}
-
-:javascript
- $(function() {
- Tree.init();
- });
diff --git a/app/views/repositories/_branch.html.haml b/app/views/repositories/_branch.html.haml
index 64a633be..3c1fe47c 100644
--- a/app/views/repositories/_branch.html.haml
+++ b/app/views/repositories/_branch.html.haml
@@ -2,12 +2,12 @@
- commit = CommitDecorator.decorate(commit)
%tr
%td
- = link_to project_commits_path(@project, ref: branch.name) do
+ = link_to project_commits_path(@project, branch.name) do
%strong= truncate(branch.name, length: 60)
- if branch.name == @project.root_ref
%span.label default
%td
- = link_to project_commit_path(@project, id: commit.id) do
+ = link_to project_commit_path(@project, commit) do
%code= commit.short_id
= image_tag gravatar_icon(commit.author_email), class: "", width: 16
diff --git a/app/views/repositories/_branches_head.html.haml b/app/views/repositories/_branches_head.html.haml
index 6afff627..25a988cf 100644
--- a/app/views/repositories/_branches_head.html.haml
+++ b/app/views/repositories/_branches_head.html.haml
@@ -1,11 +1,8 @@
= render "commits/head"
%ul.nav.nav-pills
- %li{class: ("active" if current_page?(project_repository_path(@project)))}
- = link_to project_repository_path(@project) do
- Recent
- %li{class: ("active" if current_page?(project_protected_branches_path(@project)))}
- = link_to project_protected_branches_path(@project) do
- Protected
- %li{class: ("active" if current_page?(branches_project_repository_path(@project)))}
- = link_to branches_project_repository_path(@project) do
- All
+ = nav_link(path: 'repositories#show') do
+ = link_to 'Recent', project_repository_path(@project)
+ = nav_link(path: 'protected_branches#index') do
+ = link_to 'Protected', project_protected_branches_path(@project)
+ = nav_link(path: 'repositories#branches') do
+ = link_to 'All', branches_project_repository_path(@project)
diff --git a/app/views/repositories/_feed.html.haml b/app/views/repositories/_feed.html.haml
index 0c13551d..496328ba 100644
--- a/app/views/repositories/_feed.html.haml
+++ b/app/views/repositories/_feed.html.haml
@@ -2,7 +2,7 @@
- commit = CommitDecorator.new(commit)
%tr
%td
- = link_to project_commits_path(@project, ref: commit.head.name) do
+ = link_to project_commits_path(@project, commit.head.name) do
%strong
= commit.head.name
- if commit.head.name == @project.root_ref
diff --git a/app/views/repositories/tags.html.haml b/app/views/repositories/tags.html.haml
index a4114586..38cc3aca 100644
--- a/app/views/repositories/tags.html.haml
+++ b/app/views/repositories/tags.html.haml
@@ -12,7 +12,7 @@
- commit = CommitDecorator.decorate(commit)
%tr
%td
- %strong= link_to tag.name, project_commits_path(@project, ref: tag.name), class: ""
+ %strong= link_to tag.name, project_commits_path(@project, tag.name), class: ""
%td
= link_to project_commit_path(@project, commit.id) do
%code= commit.short_id
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index e0c89522..8b44cf19 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,5 +1,5 @@
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= select_tag "ref", grouped_options_refs, class: "project-refs-select chosen"
= hidden_field_tag :destination, destination
- - if respond_to?(:path)
+ - if defined?(path)
= hidden_field_tag :path, path
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 4188a9f1..1b8701e9 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -7,14 +7,17 @@
= link_to "Edit", edit_project_snippet_path(@project, @snippet), class: "btn small right"
%br
-.file_holder
- .file_title
- %i.icon-file
- %strong= @snippet.file_name
- %span.options
- = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn very_small", target: "_blank"
- .file_content.code
- %div{class: current_user.dark_scheme ? "black" : ""}
- = raw @snippet.colorize(options: { linenos: 'True'})
+%div
+ .file_holder
+ .file_title
+ %i.icon-file
+ %strong= @snippet.file_name
+ %span.options
+ = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn very_small", target: "_blank"
+ .file_content.code
+ %div{class: current_user.dark_scheme ? "black" : ""}
+ = raw @snippet.colorize(options: { linenos: 'True'})
-= render "notes/notes_with_form", tid: @snippet.id, tt: "snippet"
+
+%div
+ = render "notes/notes_with_form", tid: @snippet.id, tt: "snippet"
diff --git a/app/views/tree/_blob.html.haml b/app/views/tree/_blob.html.haml
new file mode 100644
index 00000000..9ede3f8e
--- /dev/null
+++ b/app/views/tree/_blob.html.haml
@@ -0,0 +1,13 @@
+.file_holder
+ .file_title
+ %i.icon-file
+ %span.file_name
+ = blob.name.force_encoding('utf-8')
+ %small= number_to_human_size blob.size
+ %span.options= render "tree/blob_actions"
+ - if blob.text?
+ = render "tree/blob/text", blob: blob
+ - elsif blob.image?
+ = render "tree/blob/image", blob: blob
+ - else
+ = render "tree/blob/download", blob: blob
diff --git a/app/views/tree/_blob_actions.html.haml b/app/views/tree/_blob_actions.html.haml
new file mode 100644
index 00000000..21334ea1
--- /dev/null
+++ b/app/views/tree/_blob_actions.html.haml
@@ -0,0 +1,12 @@
+.btn-group.tree-btn-group
+ -# only show edit link for text files
+ - if @tree.text?
+ = link_to "edit", edit_project_tree_path(@project, @id), class: "btn very_small", disabled: !allowed_tree_edit?
+ = link_to "raw", project_blob_path(@project, @id), class: "btn very_small", target: "_blank"
+ -# only show normal/blame view links for text files
+ - if @tree.text?
+ - if current_page? project_blame_path(@project, @id)
+ = link_to "normal view", project_tree_path(@project, @id), class: "btn very_small"
+ - else
+ = link_to "blame", project_blame_path(@project, @id), class: "btn very_small"
+ = link_to "history", project_commits_path(@project, @id), class: "btn very_small"
diff --git a/app/views/tree/_head.html.haml b/app/views/tree/_head.html.haml
new file mode 100644
index 00000000..f1b3f63f
--- /dev/null
+++ b/app/views/tree/_head.html.haml
@@ -0,0 +1,10 @@
+%ul.nav.nav-tabs
+ %li
+ = render partial: 'shared/ref_switcher', locals: {destination: 'tree', path: @path}
+ = nav_link(controller: :tree) do
+ = link_to 'Source', project_tree_path(@project, @ref)
+ %li.right
+ .input-prepend.project_clone_holder
+ %button{class: "btn small active", :"data-clone" => @project.ssh_url_to_repo} SSH
+ %button{class: "btn small", :"data-clone" => @project.http_url_to_repo} HTTP
+ = text_field_tag :project_clone, @project.url_to_repo, class: "one_click_select span5"
diff --git a/app/views/tree/_readme.html.haml b/app/views/tree/_readme.html.haml
new file mode 100644
index 00000000..e9bb1127
--- /dev/null
+++ b/app/views/tree/_readme.html.haml
@@ -0,0 +1,13 @@
+.file_holder#README
+ .file_title
+ %i.icon-file
+ = readme.name
+ .file_content.wiki
+ - if gitlab_markdown?(readme.name)
+ = preserve do
+ = markdown(readme.data)
+ - elsif plain_text_readme?(readme.name)
+ %pre.clean
+ = readme.data
+ - else
+ = raw GitHub::Markup.render(readme.name, readme.data)
diff --git a/app/views/refs/_submodule_item.html.haml b/app/views/tree/_submodule_item.html.haml
similarity index 60%
rename from app/views/refs/_submodule_item.html.haml
rename to app/views/tree/_submodule_item.html.haml
index 6b9f5877..43fa7f24 100644
--- a/app/views/refs/_submodule_item.html.haml
+++ b/app/views/tree/_submodule_item.html.haml
@@ -1,13 +1,11 @@
-- url = content.url(@ref) rescue nil
-- name = content.basename
+- url = submodule_item.url(@ref) rescue nil
+- name = submodule_item.basename
- return unless url
%tr{ class: "tree-item", url: url }
%td.tree-item-file-name
= image_tag "submodule.png"
%strong= truncate(name, length: 40)
%td
- %code= content.id[0..10]
- %td
+ %code= submodule_item.id[0..10]
+ %td{ colspan: 2 }
= link_to truncate(url, length: 40), url
-
-
diff --git a/app/views/tree/_tree.html.haml b/app/views/tree/_tree.html.haml
new file mode 100644
index 00000000..02ae3d90
--- /dev/null
+++ b/app/views/tree/_tree.html.haml
@@ -0,0 +1,43 @@
+%ul.breadcrumb
+ %li
+ %span.arrow
+ = link_to project_tree_path(@project, @ref) do
+ = @project.name
+ - tree.breadcrumbs(6) do |link|
+ \/
+ %li= link
+
+.clear
+%div.tree_progress
+
+%div#tree-content-holder.tree-content-holder
+ - if tree.is_blob?
+ = render "tree/blob", blob: tree
+ - else
+ %table#tree-slider{class: "table_#{@hex_path} tree-table" }
+ %thead
+ %th Name
+ %th Last Update
+ %th Last Commit
+ %th= link_to "history", project_commits_path(@project, @id), class: "btn very_small right"
+
+ - if tree.up_dir?
+ %tr.tree-item
+ %td.tree-item-file-name
+ = image_tag "file_empty.png", size: '16x16'
+ = link_to "..", tree.up_dir_path
+ %td
+ %td
+ %td
+
+ = render_tree(tree.contents)
+
+ - if tree.readme
+ = render "tree/readme", readme: tree.readme
+
+- unless tree.is_blob?
+ :javascript
+ // Load last commit log for each file in tree
+ $(window).load(function(){
+ ajaxGet('#{@logs_path}');
+ });
diff --git a/app/views/tree/_tree_commit_column.html.haml b/app/views/tree/_tree_commit_column.html.haml
new file mode 100644
index 00000000..9d02132b
--- /dev/null
+++ b/app/views/tree/_tree_commit_column.html.haml
@@ -0,0 +1,2 @@
+%span.tree_author= commit.author_link avatar: true
+= link_to_gfm truncate(commit.title, length: 80), project_commit_path(@project, commit.id), class: "tree-commit-link"
diff --git a/app/views/tree/_tree_item.html.haml b/app/views/tree/_tree_item.html.haml
new file mode 100644
index 00000000..0a76d5c2
--- /dev/null
+++ b/app/views/tree/_tree_item.html.haml
@@ -0,0 +1,9 @@
+%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
+ %td.tree-item-file-name
+ = tree_icon(type)
+ %strong= link_to truncate(tree_item.name, length: 40), project_tree_path(@project, tree_join(@id || @commit.id, tree_item.name))
+ %td.tree_time_ago.cgray
+ %span.log_loading.hide
+ Loading commit data...
+ = image_tag "ajax_loader_tree.gif", width: 14
+ %td.tree_commit{ colspan: 2 }
diff --git a/app/views/tree/blob/_download.html.haml b/app/views/tree/blob/_download.html.haml
new file mode 100644
index 00000000..c3076229
--- /dev/null
+++ b/app/views/tree/blob/_download.html.haml
@@ -0,0 +1,8 @@
+.file_content.blob_file
+ %center
+ = link_to project_blob_path(@project, @id) do
+ %div.padded
+ %br
+ = image_tag "download.png", width: 64
+ %h3
+ Download (#{number_to_human_size blob.size})
diff --git a/app/views/tree/blob/_image.html.haml b/app/views/tree/blob/_image.html.haml
new file mode 100644
index 00000000..7b23f0c8
--- /dev/null
+++ b/app/views/tree/blob/_image.html.haml
@@ -0,0 +1,2 @@
+.file_content.image_file
+ %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
diff --git a/app/views/tree/blob/_text.html.haml b/app/views/tree/blob/_text.html.haml
new file mode 100644
index 00000000..c506a39f
--- /dev/null
+++ b/app/views/tree/blob/_text.html.haml
@@ -0,0 +1,15 @@
+- if gitlab_markdown?(blob.name)
+ .file_content.wiki
+ = preserve do
+ = markdown(blob.data)
+- elsif markup?(blob.name)
+ .file_content.wiki
+ = raw GitHub::Markup.render(blob.name, blob.data)
+- else
+ .file_content.code
+ - unless blob.empty?
+ %div{class: current_user.dark_scheme ? "black" : "white"}
+ = preserve do
+ = raw blob.colorize(options: { linenos: 'True'})
+ - else
+ %h4.nothing_here_message Empty file
diff --git a/app/views/tree/edit.html.haml b/app/views/tree/edit.html.haml
new file mode 100644
index 00000000..fdd334a3
--- /dev/null
+++ b/app/views/tree/edit.html.haml
@@ -0,0 +1,28 @@
+.file-editor
+ = form_tag(project_tree_path(@project, @id), :method => :put) do
+ .file_holder
+ .file_title
+ %i.icon-file
+ %span.file_name
+ = "#{@tree.path.force_encoding('utf-8')} (#{@ref})"
+ .file_content.code
+ #editor= @tree.data
+
+ .editor-commit-comment
+ = label_tag 'commit_message' do
+ %p.slead Commit message
+ = text_area_tag 'commit_message', '', :required => true
+ .form-actions
+ = hidden_field_tag 'last_commit', @last_commit
+ = hidden_field_tag 'content', '', :id => :file_content
+ = button_tag "Commit", class: 'btn save-btn'
+ = link_to "Cancel", project_tree_path(@project, @id), class: "btn cancel-btn", confirm: "Are you sure?"
+
+:javascript
+ var editor = ace.edit("editor");
+ editor.getSession().setMode("ace/mode/javascript");
+
+ $(".save-btn").click(function(){
+ $("#file_content").val(editor.getValue());
+ $(".form_editor form").submit();
+ });
diff --git a/app/views/tree/show.html.haml b/app/views/tree/show.html.haml
new file mode 100644
index 00000000..a4034f22
--- /dev/null
+++ b/app/views/tree/show.html.haml
@@ -0,0 +1,3 @@
+= render "head"
+%div#tree-holder.tree-holder
+ = render "tree", tree: @tree
diff --git a/app/views/refs/tree.js.haml b/app/views/tree/show.js.haml
similarity index 71%
rename from app/views/refs/tree.js.haml
rename to app/views/tree/show.js.haml
index 92e90579..fadd5e22 100644
--- a/app/views/refs/tree.js.haml
+++ b/app/views/tree/show.js.haml
@@ -1,8 +1,8 @@
:plain
// Load Files list
- $("#tree-holder").html("#{escape_javascript(render(partial: "tree", locals: {repo: @repo, commit: @commit, tree: @tree}))}");
+ $("#tree-holder").html("#{escape_javascript(render(partial: "tree", locals: {tree: @tree}))}");
$("#tree-content-holder").show("slide", { direction: "right" }, 150);
- $('.project-refs-form #path').val("#{params[:path]}");
+ $('.project-refs-form #path').val("#{@path}");
// Load last commit log for each file in tree
$('#tree-slider').waitForImages(function() {
diff --git a/app/views/wikis/_form.html.haml b/app/views/wikis/_form.html.haml
index b05d0a78..89bbe2ea 100644
--- a/app/views/wikis/_form.html.haml
+++ b/app/views/wikis/_form.html.haml
@@ -21,7 +21,7 @@
.bottom_box_content
= f.label :content
- .input= f.text_area :content, class: 'span8'
+ .input= f.text_area :content, class: 'span8 gfm-input'
.actions
= f.submit 'Save', class: "save-btn btn"
= link_to "Cancel", project_wiki_path(@project, :index), class: "btn cancel-btn"
diff --git a/config/application.rb b/config/application.rb
index ad41f196..74ed330e 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -23,7 +23,15 @@ module Gitlab
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
# Activate observers that should always be running.
- config.active_record.observers = :mailer_observer, :activity_observer, :project_observer, :key_observer, :issue_observer, :user_observer, :system_hook_observer, :users_project_observer
+ config.active_record.observers = :activity_observer,
+ :issue_observer,
+ :key_observer,
+ :merge_request_observer,
+ :note_observer,
+ :project_observer,
+ :system_hook_observer,
+ :user_observer,
+ :users_project_observer
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
@@ -39,13 +47,16 @@ module Gitlab
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters += [:password]
+ # Enforce whitelist mode for mass assignment.
+ # This will create an empty whitelist of attributes available for mass-assignment for all models
+ # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
+ # parameters by using an attr_accessible or attr_protected declaration.
+ config.active_record.whitelist_attributes = true
+
# Enable the asset pipeline
config.assets.enabled = true
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
-
- # Add fonts
- config.assets.paths << "#{Rails.root}/app/assets/fonts"
end
end
diff --git a/config/database.yml.example b/config/database.yml.example
index 5b5c3f0b..c5a2b8d6 100644
--- a/config/database.yml.example
+++ b/config/database.yml.example
@@ -9,12 +9,11 @@ production:
pool: 5
username: root
password: "secure password"
+ # host: localhost
# socket: /tmp/mysql.sock
-
-#
-# Development specific
#
+# Development specific
#
development:
adapter: mysql2
@@ -38,6 +37,3 @@ test: &test
username: root
password: "secure password"
# socket: /tmp/mysql.sock
-
-cucumber:
- <<: *test
diff --git a/config/database.yml.mysql b/config/database.yml.mysql
index 99cd7eaa..436bea77 100644
--- a/config/database.yml.mysql
+++ b/config/database.yml.mysql
@@ -9,12 +9,11 @@ production:
pool: 5
username: root
password: "secure password"
+ # host: localhost
# socket: /tmp/mysql.sock
-
-#
-# Development specific
#
+# Development specific
#
development:
adapter: mysql2
@@ -38,6 +37,3 @@ test: &test
username: root
password:
# socket: /tmp/mysql.sock
-
-cucumber:
- <<: *test
diff --git a/config/database.yml.postgresql b/config/database.yml.postgresql
new file mode 100644
index 00000000..17b38f3d
--- /dev/null
+++ b/config/database.yml.postgresql
@@ -0,0 +1,48 @@
+#
+# PRODUCTION
+#
+production:
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_production
+ pool: 5
+ username: postgres
+ password:
+ # host: localhost
+ # socket: /tmp/postgresql.sock
+
+#
+# Development specific
+#
+development:
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_development
+ pool: 5
+ username: postgres
+ password:
+ # socket: /tmp/postgresql.sock
+
+#
+# Staging specific
+#
+staging:
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_staging
+ pool: 5
+ username: postgres
+ password:
+ # socket: /tmp/postgresql.sock
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test: &test
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_test
+ pool: 5
+ username: postgres
+ password:
+ # socket: /tmp/postgresql.sock
diff --git a/config/database.yml.sqlite b/config/database.yml.sqlite
index 077a17ba..591448f6 100644
--- a/config/database.yml.sqlite
+++ b/config/database.yml.sqlite
@@ -12,12 +12,9 @@ production:
pool: 5
timeout: 5000
-
-#
+#
# Development specific
#
-#
-#
development:
adapter: sqlite3
database: db/development.sqlite3
@@ -32,6 +29,3 @@ test: &test
database: db/test.sqlite3
pool: 5
timeout: 5000
-
-cucumber:
- <<: *test
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 87b095e2..38400d17 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -33,7 +33,7 @@ Gitlab::Application.configure do
# Raise exception on mass assignment protection for Active Record models
config.active_record.mass_assignment_sanitizer = :strict
-
+
# Log the query plan for queries taking more than this (works
# with SQLite, MySQL, and PostgreSQL)
config.active_record.auto_explain_threshold_in_seconds = 0.5
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 1e7765d9..f5816e42 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -34,6 +34,9 @@ Gitlab::Application.configure do
# like if you have constraints or database-specific column types
# config.active_record.schema_format = :sql
+ # Raise exception on mass assignment protection for Active Record models
+ # config.active_record.mass_assignment_sanitizer = :strict
+
# Print deprecation notices to the stderr
config.active_support.deprecation = :stderr
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 7a7ca43f..fb809636 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -31,15 +31,17 @@ class Settings < Settingslogic
end
def build_url
- raw_url = self.web_protocol
- raw_url << "://"
- raw_url << web_host
-
if web_custom_port?
- raw_url << ":#{web_port}"
+ custom_port = ":#{web_port}"
+ else
+ custom_port = nil
end
-
- raw_url
+ [
+ web_protocol,
+ "://",
+ web_host,
+ custom_port
+ ].join('')
end
def ssh_port
@@ -112,7 +114,7 @@ class Settings < Settingslogic
def backup_path
t = app['backup_path'] || "backups/"
- t = /^\//.match(t) ? t : File.join(Rails.root + t)
+ t = /^\//.match(t) ? t : Rails.root .join(t)
t
end
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 9e8b0131..5d46ece1 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -8,3 +8,24 @@
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
+
+# Mark "commits" as uncountable.
+#
+# Without this change, the routes
+#
+# resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/}
+# resources :commits, only: [:show], constraints: {id: /.+/}
+#
+# would generate identical route helper methods (`project_commit_path`), resulting
+# in one of them not getting a helper method at all.
+#
+# After this change, the helper methods are:
+#
+# project_commit_path(@project, @project.commit)
+# # => "/gitlabhq/commit/bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a
+#
+# project_commits_path(@project, 'stable/README.md')
+# # => "/gitlabhq/commits/stable/README.md"
+ActiveSupport::Inflector.inflections do |inflect|
+ inflect.uncountable %w(commits)
+end
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index 72aca7e4..3549b836 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -3,3 +3,5 @@
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
# Mime::Type.register_alias "text/html", :iphone
+
+Mime::Type.register_alias 'text/plain', :patch
diff --git a/config/initializers/postgresql_limit_fix.rb b/config/initializers/postgresql_limit_fix.rb
new file mode 100644
index 00000000..0cb3aaf4
--- /dev/null
+++ b/config/initializers/postgresql_limit_fix.rb
@@ -0,0 +1,26 @@
+if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
+ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
+ class TableDefinition
+ def text(*args)
+ options = args.extract_options!
+ options.delete(:limit)
+ column_names = args
+ type = :text
+ column_names.each { |name| column(name, type, options) }
+ end
+ end
+
+ def add_column_with_limit_filter(table_name, column_name, type, options = {})
+ options.delete(:limit) if type == :text
+ add_column_without_limit_filter(table_name, column_name, type, options)
+ end
+
+ def change_column_with_limit_filter(table_name, column_name, type, options = {})
+ options.delete(:limit) if type == :text
+ change_column_without_limit_filter(table_name, column_name, type, options)
+ end
+
+ alias_method_chain :add_column, :limit_filter
+ alias_method_chain :change_column, :limit_filter
+ end
+end
diff --git a/config/initializers/quite_assets.rb b/config/initializers/quite_assets.rb
new file mode 100644
index 00000000..6fed1803
--- /dev/null
+++ b/config/initializers/quite_assets.rb
@@ -0,0 +1,13 @@
+if Rails.env.development?
+ Rails.application.assets.logger = Logger.new('/dev/null')
+ Rails::Rack::Logger.class_eval do
+ def call_with_quiet_assets(env)
+ previous_level = Rails.logger.level
+ Rails.logger.level = Logger::ERROR if env['PATH_INFO'] =~ %r{^/assets/}
+ call_without_quiet_assets(env)
+ ensure
+ Rails.logger.level = previous_level
+ end
+ alias_method_chain :call, :quiet_assets
+ end
+end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 36c5f466..e777ae2b 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -1,6 +1,8 @@
# Be sure to restart your server when you modify this file.
-Gitlab::Application.config.session_store :cookie_store, key: '_gitlab_session'
+Gitlab::Application.config.session_store :cookie_store, key: '_gitlab_session',
+ secure: Gitlab::Application.config.force_ssl,
+ httponly: true
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
diff --git a/config/routes.rb b/config/routes.rb
index cfb9bdb9..de5261d2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -43,6 +43,12 @@ Gitlab::Application.routes.draw do
put :unblock
end
end
+ resources :groups, constraints: { id: /[^\/]+/ } do
+ member do
+ put :project_update
+ delete :remove_project
+ end
+ end
resources :projects, constraints: { id: /[^\/]+/ } do
member do
get :team
@@ -81,6 +87,19 @@ Gitlab::Application.routes.draw do
get "dashboard/issues" => "dashboard#issues"
get "dashboard/merge_requests" => "dashboard#merge_requests"
+
+ #
+ # Groups Area
+ #
+ resources :groups, constraints: { id: /[^\/]+/ }, only: [:show] do
+ member do
+ get :issues
+ get :merge_requests
+ get :search
+ get :people
+ end
+ end
+
resources :projects, constraints: { id: /[^\/]+/ }, only: [:new, :create]
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks }
@@ -122,38 +141,14 @@ Gitlab::Application.routes.draw do
end
member do
- get "tree", constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }
+ # tree viewer logs
get "logs_tree", constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }
-
- get "blob",
- constraints: {
- id: /[a-zA-Z.0-9\/_\-]+/,
- path: /.*/
- }
-
- # tree viewer
- get "tree/:path" => "refs#tree",
- as: :tree_file,
- constraints: {
- id: /[a-zA-Z.0-9\/_\-]+/,
- path: /.*/
- }
-
- # tree viewer
get "logs_tree/:path" => "refs#logs_tree",
as: :logs_file,
constraints: {
id: /[a-zA-Z.0-9\/_\-]+/,
path: /.*/
}
-
- # blame
- get "blame/:path" => "refs#blame",
- as: :blame_file,
- constraints: {
- id: /[a-zA-Z.0-9\/_\-]+/,
- path: /.*/
- }
end
end
@@ -182,27 +177,28 @@ Gitlab::Application.routes.draw do
get :test
end
end
- resources :commits do
- collection do
- get :compare
- end
- member do
- get :patch
- end
- end
+ resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/}
+ resources :commits, only: [:show], constraints: {id: /.+/}
+ resources :compare, only: [:index, :create]
+ resources :blame, only: [:show], constraints: {id: /.+/}
+ resources :blob, only: [:show], constraints: {id: /.+/}
+ resources :tree, only: [:show, :edit, :update], constraints: {id: /.+/}
+ match "/compare/:from...:to" => "compare#show", as: "compare",
+ :via => [:get, :post], constraints: {from: /.+/, to: /.+/}
+
resources :team, controller: 'team_members', only: [:index]
resources :team_members
resources :milestones
resources :labels, only: [:index]
resources :issues do
-
collection do
post :sort
post :bulk_update
get :search
end
end
+
resources :notes, only: [:index, :create, :destroy] do
collection do
post :preview
diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example
index 23746d25..425dbf33 100644
--- a/config/unicorn.rb.example
+++ b/config/unicorn.rb.example
@@ -6,7 +6,7 @@ working_directory app_dir
# worker spawn times
preload_app true
-# nuke workers after 60 seconds (the default)
+# nuke workers after 30 seconds (60 is the default)
timeout 30
# listen on a Unix domain socket and/or a TCP port,
diff --git a/db/fixtures/test/001_repo.rb b/db/fixtures/test/001_repo.rb
index 67d4e7bf..18fc37cd 100644
--- a/db/fixtures/test/001_repo.rb
+++ b/db/fixtures/test/001_repo.rb
@@ -3,13 +3,13 @@ require 'fileutils'
print "Unpacking seed repository..."
SEED_REPO = 'seed_project.tar.gz'
-REPO_PATH = File.join(Rails.root, 'tmp', 'repositories')
+REPO_PATH = Rails.root.join('tmp', 'repositories')
# Make whatever directories we need to make
FileUtils.mkdir_p(REPO_PATH)
# Copy the archive to the repo path
-FileUtils.cp(File.join(Rails.root, 'spec', SEED_REPO), REPO_PATH)
+FileUtils.cp(Rails.root.join('spec', SEED_REPO), REPO_PATH)
# chdir to the repo path
FileUtils.cd(REPO_PATH) do
diff --git a/db/migrate/20121002150926_create_groups.rb b/db/migrate/20121002150926_create_groups.rb
new file mode 100644
index 00000000..ac178294
--- /dev/null
+++ b/db/migrate/20121002150926_create_groups.rb
@@ -0,0 +1,11 @@
+class CreateGroups < ActiveRecord::Migration
+ def change
+ create_table :groups do |t|
+ t.string :name, null: false
+ t.string :code, null: false
+ t.integer :owner_id, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20121002151033_add_group_id_to_project.rb b/db/migrate/20121002151033_add_group_id_to_project.rb
new file mode 100644
index 00000000..683fbfec
--- /dev/null
+++ b/db/migrate/20121002151033_add_group_id_to_project.rb
@@ -0,0 +1,5 @@
+class AddGroupIdToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :group_id, :integer
+ end
+end
diff --git a/db/migrate/20121009205010_postgres_create_integer_cast.rb b/db/migrate/20121009205010_postgres_create_integer_cast.rb
new file mode 100644
index 00000000..b9a97138
--- /dev/null
+++ b/db/migrate/20121009205010_postgres_create_integer_cast.rb
@@ -0,0 +1,15 @@
+class PostgresCreateIntegerCast < ActiveRecord::Migration
+ def up
+ execute <<-SQL
+ CREATE CAST (integer AS text) WITH INOUT AS IMPLICIT;
+ SQL
+ rescue ActiveRecord::StatementInvalid
+ end
+
+ def down
+ execute <<-SQL
+ DROP CAST (integer AS text);
+ SQL
+ rescue ActiveRecord::StatementInvalid
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 00bb5523..ec4729c0 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20120905043334) do
+ActiveRecord::Schema.define(:version => 20121002151033) do
create_table "events", :force => true do |t|
t.string "target_type"
@@ -25,6 +25,14 @@ ActiveRecord::Schema.define(:version => 20120905043334) do
t.integer "author_id"
end
+ create_table "groups", :force => true do |t|
+ t.string "name", :null => false
+ t.string "code", :null => false
+ t.integer "owner_id", :null => false
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ end
+
create_table "issues", :force => true do |t|
t.string "title"
t.integer "assignee_id"
@@ -108,6 +116,7 @@ ActiveRecord::Schema.define(:version => 20120905043334) do
t.boolean "wall_enabled", :default => true, :null => false
t.boolean "merge_requests_enabled", :default => true, :null => false
t.boolean "wiki_enabled", :default => true, :null => false
+ t.integer "group_id"
end
create_table "protected_branches", :force => true do |t|
diff --git a/doc/api/README.md b/doc/api/README.md
index 36a36f8f..19b7ff20 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -1,6 +1,6 @@
# GitLab API
-All API requests require authentication. You need to pass a `private_token` parameter to authenticate. You can find or reset your private token in your profile.
+All API requests require authentication. You need to pass a `private_token` parameter by url or header. You can find or reset your private token in your profile.
If no, or an invalid, `private_token` is provided then an error message will be returned with status code 401:
diff --git a/doc/api/projects.md b/doc/api/projects.md
index d06a41c2..fdedf904 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -196,9 +196,9 @@ Parameters:
Status code `200` will be returned on success.
-## Get project hooks
+## List project hooks
-Get hooks for project
+Get list for project hooks
```
GET /projects/:id/hooks
@@ -210,6 +210,21 @@ Parameters:
Will return hooks with status `200 OK` on success, or `404 Not found` on fail.
+## Get project hook
+
+Get hook for project
+
+```
+GET /projects/:id/hooks/:hook_id
+```
+
+Parameters:
+
++ `id` (required) - The ID or code name of a project
++ `hook_id` (required) - The ID of a project hook
+
+Will return hook with status `200 OK` on success, or `404 Not found` on fail.
+
## Add project hook
Add hook to project
@@ -225,6 +240,23 @@ Parameters:
Will return status `201 Created` on success, or `404 Not found` on fail.
+## Edit project hook
+
+Edit hook for project
+
+```
+PUT /projects/:id/hooks/:hook_id
+```
+
+Parameters:
+
++ `id` (required) - The ID or code name of a project
++ `hook_id` (required) - The ID of a project hook
++ `url` (required) - The hook URL
+
+Will return status `201 Created` on success, or `404 Not found` on fail.
+
+
## Delete project hook
Delete hook from project
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index 0cd29ce5..288fd529 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -1,6 +1,14 @@
## List snippets
-Not implemented.
+Get a list of project snippets.
+
+```
+GET /projects/:id/snippets
+```
+
+Parameters:
+
++ `id` (required) - The ID or code name of a project
## Single snippet
diff --git a/doc/api/users.md b/doc/api/users.md
index 4f806b14..63271ee8 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -65,6 +65,27 @@ Parameters:
}
```
+## User creation
+Create user. Available only for admin
+
+```
+POST /users
+```
+
+Parameters:
++ `email` (required) - Email
++ `name` (required) - Name
++ `password` (required) - Password
++ `password_confirmation` (required) - Password confirmation
++ `skype` - Skype ID
++ `linkedin` - Linkedin
++ `twitter` - Twitter account
++ `projects_limit` - Limit projects wich user can create
+
+
+Will return created user with status `201 Created` on success, or `404 Not
+found` on fail.
+
## Current user
Get currently authenticated user.
diff --git a/doc/installation.md b/doc/installation.md
index 865cde3c..282cad95 100644
--- a/doc/installation.md
+++ b/doc/installation.md
@@ -22,6 +22,12 @@ You might have some luck using these, but no guarantees:
GitLab does **not** run on Windows and we have no plans of making GitLab compatible.
+
+## Hardware:
+
+We recommend to use server with at least 1GB RAM for gitlab instance.
+
+
## This installation guide created for Debian/Ubuntu and properly tested.
The installation consists of 6 steps:
@@ -70,11 +76,56 @@ Now install the required packages:
sudo apt-get update
sudo apt-get upgrade
- sudo apt-get install -y wget curl gcc checkinstall libxml2-dev libxslt-dev sqlite3 libsqlite3-dev libcurl4-openssl-dev libreadline6-dev libc6-dev libssl-dev libmysql++-dev make build-essential zlib1g-dev libicu-dev redis-server openssh-server git-core python-dev python-pip libyaml-dev postfix
+ sudo apt-get install -y wget curl gcc checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libreadline6-dev libc6-dev libssl-dev libmysql++-dev make build-essential zlib1g-dev libicu-dev redis-server openssh-server git-core python-dev python-pip libyaml-dev postfix libpq-dev
+
+
+# Database
+
+## SQLite
+
+ sudo apt-get install -y sqlite3 libsqlite3-dev
+
+## MySQL
- # If you want to use MySQL:
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
+ # Login to MySQL
+ $ mysql -u root -p
+
+ # Create the GitLab production database
+ mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
+
+ # Create the MySQL User change $password to a real password
+ mysql> CREATE USER 'gitlab'@'localhost' IDENTIFIED BY '$password';
+
+ # Grant proper permissions to the MySQL User
+ mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON `gitlabhq_production`.* TO 'gitlab'@'localhost';
+
+
+## PostgreSQL
+
+ sudo apt-get install -y postgresql-9.2 postgresql-server-dev-9.2
+
+ # Connect to database server
+ sudo -u postgres psql -d template1
+
+ # Add a user called gitlab. Change $password to a real password
+ template1=# CREATE USER gitlab WITH PASSWORD '$password';
+
+ # Create the GitLab production database
+ template1=# CREATE DATABASE IF NOT EXISTS gitlabhq_production;
+
+ # Grant all privileges on database
+ template1=# GRANT ALL PRIVILEGES ON DATABASE gitlabhq_production to gitlab;
+
+ # Quit from PostgreSQL server
+ template1=# \q
+
+ # Try connect to new database
+ $ su - gitlab
+ $ psql -d gitlabhq_production -U gitlab
+
+
# 2. Install Ruby
wget http://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.3-p194.tar.gz
@@ -112,7 +163,6 @@ Generate key:
Clone GitLab's fork of the Gitolite source code:
- cd /home/git
sudo -H -u git git clone -b gl-v304 https://github.com/gitlabhq/gitolite.git /home/git/gitolite
Setup:
@@ -127,6 +177,7 @@ Setup:
sudo -u git -H sh -c "PATH=/home/git/bin:$PATH; gitolite setup -pk /home/git/gitlab.pub"
sudo -u git -H sed -i 's/0077/0007/g' /home/git/.gitolite.rc
+ sudo -u git -H sed -i "s/\(GIT_CONFIG_KEYS\s*=>*\s*\).\{2\}/\1'\.\*'/g" /home/git/.gitolite.rc
Permissions:
@@ -170,28 +221,24 @@ and ensure you have followed all of the above steps carefully.
# SQLite
sudo -u gitlab cp config/database.yml.sqlite config/database.yml
- # Or
# Mysql
- # Install MySQL as directed in Step #1
+ sudo -u gitlab cp config/database.yml.mysql config/database.yml
- # Login to MySQL
- $ mysql -u root -p
+ # PostgreSQL
+ sudo -u gitlab cp config/database.yml.postgres config/database.yml
- # Create the GitLab production database
- mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
-
- # Create the MySQL User change $password to a real password
- mysql> CREATE USER 'gitlab'@'localhost' IDENTIFIED BY '$password';
-
- # Grant proper permissions to the MySQL User
- mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON `gitlabhq_production`.* TO 'gitlab'@'localhost';
-
- # Exit MySQL Server and copy the example config, make sure to update username/password in config/database.yml
- sudo -u gitlab cp config/database.yml.example config/database.yml
+ # make sure to update username/password in config/database.yml
#### Install gems
- sudo -u gitlab -H bundle install --without development test --deployment
+ # mysql
+ sudo -u gitlab -H bundle install --without development test sqlite postgres --deployment
+
+ # or postgres
+ sudo -u gitlab -H bundle install --without development test sqlite mysql --deployment
+
+ # or sqlite
+ sudo -u gitlab -H bundle install --without development test mysql postgres --deployment
#### Setup database
@@ -285,7 +332,7 @@ a different host, you can configure its connection string in the
sudo vim /etc/nginx/sites-enabled/gitlab
# Restart nginx:
- /etc/init.d/nginx restart
+ sudo /etc/init.d/nginx restart
## 3. Init script
@@ -296,7 +343,7 @@ Create init script in /etc/init.d/gitlab:
GitLab autostart:
- sudo update-rc.d gitlab defaults
+ sudo update-rc.d gitlab defaults 21
Now you can start/restart/stop GitLab like:
diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature
new file mode 100644
index 00000000..fce85ce9
--- /dev/null
+++ b/features/admin/active_tab.feature
@@ -0,0 +1,33 @@
+Feature: Admin active tab
+ Background:
+ Given I sign in as an admin
+
+ Scenario: On Admin Home
+ Given I visit admin page
+ Then the active main tab should be Home
+ And no other main tabs should be active
+
+ Scenario: On Admin Projects
+ Given I visit admin projects page
+ Then the active main tab should be Projects
+ And no other main tabs should be active
+
+ Scenario: On Admin Users
+ Given I visit admin users page
+ Then the active main tab should be Users
+ And no other main tabs should be active
+
+ Scenario: On Admin Logs
+ Given I visit admin logs page
+ Then the active main tab should be Logs
+ And no other main tabs should be active
+
+ Scenario: On Admin Hooks
+ Given I visit admin hooks page
+ Then the active main tab should be Hooks
+ And no other main tabs should be active
+
+ Scenario: On Admin Resque
+ Given I visit admin Resque page
+ Then the active main tab should be Resque
+ And no other main tabs should be active
diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature
new file mode 100644
index 00000000..6715ea26
--- /dev/null
+++ b/features/dashboard/active_tab.feature
@@ -0,0 +1,28 @@
+Feature: Dashboard active tab
+ Background:
+ Given I sign in as a user
+
+ Scenario: On Dashboard Home
+ Given I visit dashboard page
+ Then the active main tab should be Home
+ And no other main tabs should be active
+
+ Scenario: On Dashboard Issues
+ Given I visit dashboard issues page
+ Then the active main tab should be Issues
+ And no other main tabs should be active
+
+ Scenario: On Dashboard Merge Requests
+ Given I visit dashboard merge requests page
+ Then the active main tab should be Merge Requests
+ And no other main tabs should be active
+
+ Scenario: On Dashboard Search
+ Given I visit dashboard search page
+ Then the active main tab should be Search
+ And no other main tabs should be active
+
+ Scenario: On Dashboard Help
+ Given I visit dashboard help page
+ Then the active main tab should be Help
+ And no other main tabs should be active
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index 9756bc7f..24296f46 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -10,6 +10,11 @@ Feature: Dashboard
Then I should see "Shop" project link
Then I should see project "Shop" activity feed
+ Scenario: I should see groups list
+ Given I have group with projects
+ And I visit dashboard page
+ Then I should see groups list
+
Scenario: I should see last push widget
Then I should see last push widget
And I click "Create Merge Request" link
@@ -18,10 +23,10 @@ Feature: Dashboard
Scenario: I should see User joined Project event
Given user with name "John Doe" joined project "Shop"
When I visit dashboard page
- Then I should see "John Doe joined project Shop" event
+ Then I should see "John Doe joined project at Shop" event
Scenario: I should see User left Project event
Given user with name "John Doe" joined project "Shop"
And user with name "John Doe" left project "Shop"
When I visit dashboard page
- Then I should see "John Doe left project Shop" event
+ Then I should see "John Doe left project at Shop" event
diff --git a/features/group/group.feature b/features/group/group.feature
new file mode 100644
index 00000000..dbddb92c
--- /dev/null
+++ b/features/group/group.feature
@@ -0,0 +1,9 @@
+Feature: Groups
+ Background:
+ Given I sign in as a user
+ And I have group with projects
+
+ Scenario: I should see group dashboard list
+ When I visit group page
+ Then I should see projects list
+ And I should see projects activity feed
diff --git a/features/profile/active_tab.feature b/features/profile/active_tab.feature
new file mode 100644
index 00000000..475641a3
--- /dev/null
+++ b/features/profile/active_tab.feature
@@ -0,0 +1,28 @@
+Feature: Profile active tab
+ Background:
+ Given I sign in as a user
+
+ Scenario: On Profile Home
+ Given I visit profile page
+ Then the active main tab should be Home
+ And no other main tabs should be active
+
+ Scenario: On Profile Account
+ Given I visit profile account page
+ Then the active main tab should be Account
+ And no other main tabs should be active
+
+ Scenario: On Profile SSH Keys
+ Given I visit profile SSH keys page
+ Then the active main tab should be SSH Keys
+ And no other main tabs should be active
+
+ Scenario: On Profile Design
+ Given I visit profile design page
+ Then the active main tab should be Design
+ And no other main tabs should be active
+
+ Scenario: On Profile History
+ Given I visit profile history page
+ Then the active main tab should be History
+ And no other main tabs should be active
diff --git a/features/profile/profile.feature b/features/profile/profile.feature
index 134cabb5..d07a6db1 100644
--- a/features/profile/profile.feature
+++ b/features/profile/profile.feature
@@ -20,3 +20,8 @@ Feature: Profile
Given I visit profile account page
Then I reset my token
And I should see new token
+
+ Scenario: I visit history tab
+ Given I have activity
+ When I visit profile history page
+ Then I should see my activity
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
new file mode 100644
index 00000000..2d3e41d3
--- /dev/null
+++ b/features/project/active_tab.feature
@@ -0,0 +1,147 @@
+Feature: Project active tab
+ Background:
+ Given I sign in as a user
+ And I own a project
+
+ # Main Tabs
+
+ Scenario: On Project Home
+ Given I visit my project's home page
+ Then the active main tab should be Home
+ And no other main tabs should be active
+
+ Scenario: On Project Files
+ Given I visit my project's files page
+ Then the active main tab should be Files
+ And no other main tabs should be active
+
+ Scenario: On Project Commits
+ Given I visit my project's commits page
+ Then the active main tab should be Commits
+ And no other main tabs should be active
+
+ Scenario: On Project Network
+ Given I visit my project's network page
+ Then the active main tab should be Network
+ And no other main tabs should be active
+
+ Scenario: On Project Issues
+ Given I visit my project's issues page
+ Then the active main tab should be Issues
+ And no other main tabs should be active
+
+ Scenario: On Project Merge Requests
+ Given I visit my project's merge requests page
+ Then the active main tab should be Merge Requests
+ And no other main tabs should be active
+
+ Scenario: On Project Wall
+ Given I visit my project's wall page
+ Then the active main tab should be Wall
+ And no other main tabs should be active
+
+ Scenario: On Project Wiki
+ Given I visit my project's wiki page
+ Then the active main tab should be Wiki
+ And no other main tabs should be active
+
+ # Sub Tabs: Home
+
+ Scenario: On Project Home/Show
+ Given I visit my project's home page
+ Then the active sub tab should be Show
+ And no other sub tabs should be active
+ And the active main tab should be Home
+
+ Scenario: On Project Home/Team
+ Given I visit my project's home page
+ And I click the "Team" tab
+ Then the active sub tab should be Team
+ And no other sub tabs should be active
+ And the active main tab should be Home
+
+ Scenario: On Project Home/Attachments
+ Given I visit my project's home page
+ And I click the "Attachments" tab
+ Then the active sub tab should be Attachments
+ And no other sub tabs should be active
+ And the active main tab should be Home
+
+ Scenario: On Project Home/Snippets
+ Given I visit my project's home page
+ And I click the "Snippets" tab
+ Then the active sub tab should be Snippets
+ And no other sub tabs should be active
+ And the active main tab should be Home
+
+ Scenario: On Project Home/Edit
+ Given I visit my project's home page
+ And I click the "Edit" tab
+ Then the active sub tab should be Edit
+ And no other sub tabs should be active
+ And the active main tab should be Home
+
+ Scenario: On Project Home/Hooks
+ Given I visit my project's home page
+ And I click the "Hooks" tab
+ Then the active sub tab should be Hooks
+ And no other sub tabs should be active
+ And the active main tab should be Home
+
+ Scenario: On Project Home/Deploy Keys
+ Given I visit my project's home page
+ And I click the "Deploy Keys" tab
+ Then the active sub tab should be Deploy Keys
+ And no other sub tabs should be active
+ And the active main tab should be Home
+
+ # Sub Tabs: Commits
+
+ Scenario: On Project Commits/Commits
+ Given I visit my project's commits page
+ Then the active sub tab should be Commits
+ And no other sub tabs should be active
+ And the active main tab should be Commits
+
+ Scenario: On Project Commits/Compare
+ Given I visit my project's commits page
+ And I click the "Compare" tab
+ Then the active sub tab should be Compare
+ And no other sub tabs should be active
+ And the active main tab should be Commits
+
+ Scenario: On Project Commits/Branches
+ Given I visit my project's commits page
+ And I click the "Branches" tab
+ Then the active sub tab should be Branches
+ And no other sub tabs should be active
+ And the active main tab should be Commits
+
+ Scenario: On Project Commits/Tags
+ Given I visit my project's commits page
+ And I click the "Tags" tab
+ Then the active sub tab should be Tags
+ And no other sub tabs should be active
+ And the active main tab should be Commits
+
+ # Sub Tabs: Issues
+
+ Scenario: On Project Issues/Browse
+ Given I visit my project's issues page
+ Then the active sub tab should be Browse Issues
+ And no other sub tabs should be active
+ And the active main tab should be Issues
+
+ Scenario: On Project Issues/Milestones
+ Given I visit my project's issues page
+ And I click the "Milestones" tab
+ Then the active sub tab should be Milestones
+ And no other sub tabs should be active
+ And the active main tab should be Issues
+
+ Scenario: On Project Issues/Labels
+ Given I visit my project's issues page
+ And I click the "Labels" tab
+ Then the active sub tab should be Labels
+ And no other sub tabs should be active
+ And the active main tab should be Issues
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 53de6e6a..df795ef7 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -1,8 +1,8 @@
Feature: Project Browse commits
Background:
Given I sign in as a user
- And I own project "Shop"
- Given I visit project commits page
+ And I own a project
+ And I visit my project's commits page
Scenario: I browse commits list for master branch
Then I see project commits
@@ -18,4 +18,4 @@ Feature: Project Browse commits
Scenario: I compare refs
Given I visit compare refs page
And I fill compare fields with refs
- And I see compared refs
+ Then I see compared refs
diff --git a/features/project/hooks.feature b/features/project/hooks.feature
new file mode 100644
index 00000000..b158e07a
--- /dev/null
+++ b/features/project/hooks.feature
@@ -0,0 +1,21 @@
+Feature: Project Hooks
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+
+ Scenario: I should see hook list
+ Given project has hook
+ When I visit project hooks page
+ Then I should see project hook
+
+ Scenario: I add new hook
+ Given I visit project hooks page
+ When I submit new hook
+ Then I should see newly created hook
+
+ Scenario: I test hook
+ Given project has hook
+ And I visit project hooks page
+ When I click test hook button
+ Then hook should be triggered
+
diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature
index a57f67d6..50c090cc 100644
--- a/features/project/issues/milestones.feature
+++ b/features/project/issues/milestones.feature
@@ -16,3 +16,11 @@ Feature: Project Milestones
Given I click link "New Milestone"
And I submit new milestone "v2.3"
Then I should see milestone "v2.3"
+
+ @javascript
+ Scenario: Listing closed issues
+ Given the milestone has open and closed issues
+ And I click link "v2.2"
+ Then I should see 3 issues
+ When I click link "All Issues"
+ Then I should see 4 issues
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index b12b0ee3..0b8495ff 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -19,3 +19,9 @@ Feature: Project Browse files
Given I visit blob file from repo
And I click link "raw"
Then I should see raw file content
+
+ @javascript
+ Scenario: I can edit file
+ Given I click on "Gemfile" file in repo
+ And I click button "edit"
+ Then I can edit code
diff --git a/features/steps/admin/admin_active_tab.rb b/features/steps/admin/admin_active_tab.rb
new file mode 100644
index 00000000..29290892
--- /dev/null
+++ b/features/steps/admin/admin_active_tab.rb
@@ -0,0 +1,29 @@
+class AdminActiveTab < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedActiveTab
+
+ Then 'the active main tab should be Home' do
+ ensure_active_main_tab('Stats')
+ end
+
+ Then 'the active main tab should be Projects' do
+ ensure_active_main_tab('Projects')
+ end
+
+ Then 'the active main tab should be Users' do
+ ensure_active_main_tab('Users')
+ end
+
+ Then 'the active main tab should be Logs' do
+ ensure_active_main_tab('Logs')
+ end
+
+ Then 'the active main tab should be Hooks' do
+ ensure_active_main_tab('Hooks')
+ end
+
+ Then 'the active main tab should be Resque' do
+ ensure_active_main_tab('Resque')
+ end
+end
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index 154b97e3..a9416f73 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -41,8 +41,8 @@ class Dashboard < Spinach::FeatureSteps
)
end
- Then 'I should see "John Doe joined project Shop" event' do
- page.should have_content "John Doe joined project Shop"
+ Then 'I should see "John Doe joined project at Shop" event' do
+ page.should have_content "John Doe joined project at Shop"
end
And 'user with name "John Doe" left project "Shop"' do
@@ -55,8 +55,8 @@ class Dashboard < Spinach::FeatureSteps
)
end
- Then 'I should see "John Doe left project Shop" event' do
- page.should have_content "John Doe left project Shop"
+ Then 'I should see "John Doe left project at Shop" event' do
+ page.should have_content "John Doe left project at Shop"
end
And 'I own project "Shop"' do
@@ -64,6 +64,14 @@ class Dashboard < Spinach::FeatureSteps
@project.add_access(@user, :admin)
end
+ And 'I have group with projects' do
+ @group = Factory :group
+ @project = Factory :project, group: @group
+ @event = Factory :closed_issue_event, project: @project
+
+ @project.add_access current_user, :admin
+ end
+
And 'project "Shop" has push event' do
@project = Project.find_by_name("Shop")
@@ -89,4 +97,10 @@ class Dashboard < Spinach::FeatureSteps
:author_id => @user.id
)
end
+
+ Then 'I should see groups list' do
+ Group.all.each do |group|
+ page.should have_link group.name
+ end
+ end
end
diff --git a/features/steps/dashboard/dashboard_active_tab.rb b/features/steps/dashboard/dashboard_active_tab.rb
new file mode 100644
index 00000000..41ecc48c
--- /dev/null
+++ b/features/steps/dashboard/dashboard_active_tab.rb
@@ -0,0 +1,25 @@
+class DashboardActiveTab < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedActiveTab
+
+ Then 'the active main tab should be Home' do
+ ensure_active_main_tab('Home')
+ end
+
+ Then 'the active main tab should be Issues' do
+ ensure_active_main_tab('Issues')
+ end
+
+ Then 'the active main tab should be Merge Requests' do
+ ensure_active_main_tab('Merge Requests')
+ end
+
+ Then 'the active main tab should be Search' do
+ ensure_active_main_tab('Search')
+ end
+
+ Then 'the active main tab should be Help' do
+ ensure_active_main_tab('Help')
+ end
+end
diff --git a/features/steps/group/group.rb b/features/steps/group/group.rb
new file mode 100644
index 00000000..798c62c3
--- /dev/null
+++ b/features/steps/group/group.rb
@@ -0,0 +1,32 @@
+class Groups < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+
+ When 'I visit group page' do
+ visit group_path(current_group)
+ end
+
+ Then 'I should see projects list' do
+ current_user.projects.each do |project|
+ page.should have_link project.name
+ end
+ end
+
+ And 'I have group with projects' do
+ @group = Factory :group
+ @project = Factory :project, group: @group
+ @event = Factory :closed_issue_event, project: @project
+
+ @project.add_access current_user, :admin
+ end
+
+ And 'I should see projects activity feed' do
+ page.should have_content 'closed issue'
+ end
+
+ protected
+
+ def current_group
+ @group ||= Group.first
+ end
+end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index d3261a16..605936ba 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -41,4 +41,12 @@ class Profile < Spinach::FeatureSteps
find("#token").value.should_not == @old_token
find("#token").value.should == @user.reload.private_token
end
+
+ Given 'I have activity' do
+ Factory :closed_issue_event, author: current_user
+ end
+
+ Then 'I should see my activity' do
+ page.should have_content "#{current_user.name} closed issue"
+ end
end
diff --git a/features/steps/profile/profile_active_tab.rb b/features/steps/profile/profile_active_tab.rb
new file mode 100644
index 00000000..1924a6fa
--- /dev/null
+++ b/features/steps/profile/profile_active_tab.rb
@@ -0,0 +1,25 @@
+class ProfileActiveTab < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedActiveTab
+
+ Then 'the active main tab should be Home' do
+ ensure_active_main_tab('Profile')
+ end
+
+ Then 'the active main tab should be Account' do
+ ensure_active_main_tab('Account')
+ end
+
+ Then 'the active main tab should be SSH Keys' do
+ ensure_active_main_tab('SSH Keys')
+ end
+
+ Then 'the active main tab should be Design' do
+ ensure_active_main_tab('Design')
+ end
+
+ Then 'the active main tab should be History' do
+ ensure_active_main_tab('History')
+ end
+end
diff --git a/features/steps/profile/profile_ssh_keys.rb b/features/steps/profile/profile_ssh_keys.rb
index 96df2d73..535c3862 100644
--- a/features/steps/profile/profile_ssh_keys.rb
+++ b/features/steps/profile/profile_ssh_keys.rb
@@ -13,7 +13,7 @@ class ProfileSshKeys < Spinach::FeatureSteps
And 'I submit new ssh key "Laptop"' do
fill_in "key_title", :with => "Laptop"
- fill_in "key_key", :with => "ssh-rsa publickey234="
+ fill_in "key_key", :with => "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
click_button "Save"
end
diff --git a/features/steps/project/project_active_tab.rb b/features/steps/project/project_active_tab.rb
new file mode 100644
index 00000000..a5c80353
--- /dev/null
+++ b/features/steps/project/project_active_tab.rb
@@ -0,0 +1,146 @@
+class ProjectActiveTab < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedProject
+ include SharedActiveTab
+
+ # Main Tabs
+
+ Then 'the active main tab should be Home' do
+ ensure_active_main_tab(@project.name)
+ end
+
+ Then 'the active main tab should be Files' do
+ ensure_active_main_tab('Files')
+ end
+
+ Then 'the active main tab should be Commits' do
+ ensure_active_main_tab('Commits')
+ end
+
+ Then 'the active main tab should be Network' do
+ ensure_active_main_tab('Network')
+ end
+
+ Then 'the active main tab should be Issues' do
+ ensure_active_main_tab('Issues')
+ end
+
+ Then 'the active main tab should be Merge Requests' do
+ ensure_active_main_tab('Merge Requests')
+ end
+
+ Then 'the active main tab should be Wall' do
+ ensure_active_main_tab('Wall')
+ end
+
+ Then 'the active main tab should be Wiki' do
+ ensure_active_main_tab('Wiki')
+ end
+
+ # Sub Tabs: Home
+
+ Given 'I click the "Team" tab' do
+ click_link('Team')
+ end
+
+ Given 'I click the "Attachments" tab' do
+ click_link('Attachments')
+ end
+
+ Given 'I click the "Snippets" tab' do
+ click_link('Snippets')
+ end
+
+ Given 'I click the "Edit" tab' do
+ click_link('Edit')
+ end
+
+ Given 'I click the "Hooks" tab' do
+ click_link('Hooks')
+ end
+
+ Given 'I click the "Deploy Keys" tab' do
+ click_link('Deploy Keys')
+ end
+
+ Then 'the active sub tab should be Show' do
+ ensure_active_sub_tab('Show')
+ end
+
+ Then 'the active sub tab should be Team' do
+ ensure_active_sub_tab('Team')
+ end
+
+ Then 'the active sub tab should be Attachments' do
+ ensure_active_sub_tab('Attachments')
+ end
+
+ Then 'the active sub tab should be Snippets' do
+ ensure_active_sub_tab('Snippets')
+ end
+
+ Then 'the active sub tab should be Edit' do
+ ensure_active_sub_tab('Edit')
+ end
+
+ Then 'the active sub tab should be Hooks' do
+ ensure_active_sub_tab('Hooks')
+ end
+
+ Then 'the active sub tab should be Deploy Keys' do
+ ensure_active_sub_tab('Deploy Keys')
+ end
+
+ # Sub Tabs: Commits
+
+ Given 'I click the "Compare" tab' do
+ click_link('Compare')
+ end
+
+ Given 'I click the "Branches" tab' do
+ click_link('Branches')
+ end
+
+ Given 'I click the "Tags" tab' do
+ click_link('Tags')
+ end
+
+ Then 'the active sub tab should be Commits' do
+ ensure_active_sub_tab('Commits')
+ end
+
+ Then 'the active sub tab should be Compare' do
+ ensure_active_sub_tab('Compare')
+ end
+
+ Then 'the active sub tab should be Branches' do
+ ensure_active_sub_tab('Branches')
+ end
+
+ Then 'the active sub tab should be Tags' do
+ ensure_active_sub_tab('Tags')
+ end
+
+ # Sub Tabs: Issues
+
+ Given 'I click the "Milestones" tab' do
+ click_link('Milestones')
+ end
+
+ Given 'I click the "Labels" tab' do
+ click_link('Labels')
+ end
+
+ Then 'the active sub tab should be Browse Issues' do
+ ensure_active_sub_tab('Browse Issues')
+ end
+
+ Then 'the active sub tab should be Milestones' do
+ ensure_active_sub_tab('Milestones')
+ end
+
+ Then 'the active sub tab should be Labels' do
+ ensure_active_sub_tab('Labels')
+ end
+end
diff --git a/features/steps/project/project_browse_commits.rb b/features/steps/project/project_browse_commits.rb
index 01479987..cb5cabe9 100644
--- a/features/steps/project/project_browse_commits.rb
+++ b/features/steps/project/project_browse_commits.rb
@@ -4,8 +4,6 @@ class ProjectBrowseCommits < Spinach::FeatureSteps
include SharedPaths
Then 'I see project commits' do
- current_path.should == project_commits_path(@project)
-
commit = @project.commit
page.should have_content(@project.name)
page.should have_content(commit.message)
@@ -34,14 +32,14 @@ class ProjectBrowseCommits < Spinach::FeatureSteps
end
And 'I fill compare fields with refs' do
- fill_in "from", :with => "master"
- fill_in "to", :with => "stable"
+ fill_in "from", with: "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a"
+ fill_in "to", with: "8716fc78f3c65bbf7bcf7b574febd583bc5d2812"
click_button "Compare"
end
- And 'I see compared refs' do
- page.should have_content "Commits (27)"
+ Then 'I see compared refs' do
page.should have_content "Compare View"
- page.should have_content "Showing 73 changed files"
+ page.should have_content "Commits (1)"
+ page.should have_content "Showing 2 changed files"
end
end
diff --git a/features/steps/project/project_browse_files.rb b/features/steps/project/project_browse_files.rb
index 67c553ce..4efce0dc 100644
--- a/features/steps/project/project_browse_files.rb
+++ b/features/steps/project/project_browse_files.rb
@@ -5,14 +5,14 @@ class ProjectBrowseFiles < Spinach::FeatureSteps
Then 'I should see files from repository' do
page.should have_content "app"
- page.should have_content "History"
+ page.should have_content "history"
page.should have_content "Gemfile"
end
Then 'I should see files from repository for "8470d70"' do
- current_path.should == tree_project_ref_path(@project, "8470d70")
+ current_path.should == project_tree_path(@project, "8470d70")
page.should have_content "app"
- page.should have_content "History"
+ page.should have_content "history"
page.should have_content "Gemfile"
end
@@ -31,4 +31,14 @@ class ProjectBrowseFiles < Spinach::FeatureSteps
Then 'I should see raw file content' do
page.source.should == ValidCommit::BLOB_FILE
end
+
+ Given 'I click button "edit"' do
+ click_link 'edit'
+ end
+
+ Then 'I can edit code' do
+ page.execute_script('editor.setValue("GitlabFileEditor")')
+ page.evaluate_script('editor.getValue()').should == "GitlabFileEditor"
+ end
+
end
diff --git a/features/steps/project/project_hooks.rb b/features/steps/project/project_hooks.rb
new file mode 100644
index 00000000..1786fe5b
--- /dev/null
+++ b/features/steps/project/project_hooks.rb
@@ -0,0 +1,36 @@
+class ProjectHooks < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedPaths
+ include RSpec::Matchers
+ include RSpec::Mocks::ExampleMethods
+
+ Given 'project has hook' do
+ @hook = Factory :project_hook, project: current_project
+ end
+
+ Then 'I should see project hook' do
+ page.should have_content @hook.url
+ end
+
+ When 'I submit new hook' do
+ @url = Faker::Internet.uri("http")
+ fill_in "hook_url", with: @url
+ expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1)
+ end
+
+ Then 'I should see newly created hook' do
+ page.current_path.should == project_hooks_path(current_project)
+ page.should have_content(@url)
+ end
+
+ When 'I click test hook button' do
+ test_hook_context = double(execute: true)
+ TestHookContext.should_receive(:new).and_return(test_hook_context)
+ click_link 'Test Hook'
+ end
+
+ Then 'hook should be triggered' do
+ page.current_path.should == project_hooks_path(current_project)
+ end
+end
diff --git a/features/steps/project/project_milestones.rb b/features/steps/project/project_milestones.rb
index 83ed6859..4d689c95 100644
--- a/features/steps/project/project_milestones.rb
+++ b/features/steps/project/project_milestones.rb
@@ -36,4 +36,26 @@ class ProjectMilestones < Spinach::FeatureSteps
3.times { Factory :issue, :project => project, :milestone => milestone }
end
+
+ Given 'the milestone has open and closed issues' do
+ project = Project.find_by_name("Shop")
+ milestone = project.milestones.find_by_title('v2.2')
+
+ # 3 Open issues created above; create one closed issue
+ create(:closed_issue, project: project, milestone: milestone)
+ end
+
+ When 'I click link "All Issues"' do
+ click_link 'All Issues'
+ end
+
+ Then "I should see 3 issues" do
+ page.should have_selector('.milestone-issue-filter tbody tr', count: 4)
+ page.should have_selector('.milestone-issue-filter tbody tr.hide', count: 1)
+ end
+
+ Then "I should see 4 issues" do
+ page.should have_selector('.milestone-issue-filter tbody tr', count: 4)
+ page.should_not have_selector('.milestone-issue-filter tbody tr.hide')
+ end
end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
new file mode 100644
index 00000000..884f2d5f
--- /dev/null
+++ b/features/steps/shared/active_tab.rb
@@ -0,0 +1,19 @@
+module SharedActiveTab
+ include Spinach::DSL
+
+ def ensure_active_main_tab(content)
+ page.find('ul.main_menu li.active').should have_content(content)
+ end
+
+ def ensure_active_sub_tab(content)
+ page.find('div.content ul.nav-tabs li.active').should have_content(content)
+ end
+
+ And 'no other main tabs should be active' do
+ page.should have_selector('ul.main_menu li.active', count: 1)
+ end
+
+ And 'no other sub tabs should be active' do
+ page.should have_selector('div.content ul.nav-tabs li.active', count: 1)
+ end
+end
diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb
index 77d9839f..8c501bbc 100644
--- a/features/steps/shared/authentication.rb
+++ b/features/steps/shared/authentication.rb
@@ -7,4 +7,12 @@ module SharedAuthentication
Given 'I sign in as a user' do
login_as :user
end
+
+ Given 'I sign in as an admin' do
+ login_as :admin
+ end
+
+ def current_user
+ @user || User.first
+ end
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 93ad0219..eda5ab94 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -1,22 +1,38 @@
module SharedPaths
include Spinach::DSL
- And 'I visit dashboard search page' do
- visit search_path
+ When 'I visit new project page' do
+ visit new_project_path
end
- And 'I visit dashboard merge requests page' do
- visit dashboard_merge_requests_path
+ # ----------------------------------------
+ # Dashboard
+ # ----------------------------------------
+
+ Given 'I visit dashboard page' do
+ visit dashboard_path
end
- And 'I visit dashboard issues page' do
+ Given 'I visit dashboard issues page' do
visit dashboard_issues_path
end
- When 'I visit dashboard page' do
- visit dashboard_path
+ Given 'I visit dashboard merge requests page' do
+ visit dashboard_merge_requests_path
end
+ Given 'I visit dashboard search page' do
+ visit search_path
+ end
+
+ Given 'I visit dashboard help page' do
+ visit help_path
+ end
+
+ # ----------------------------------------
+ # Profile
+ # ----------------------------------------
+
Given 'I visit profile page' do
visit profile_path
end
@@ -25,14 +41,98 @@ module SharedPaths
visit profile_account_path
end
+ Given 'I visit profile SSH keys page' do
+ visit keys_path
+ end
+
+ Given 'I visit profile design page' do
+ visit profile_design_path
+ end
+
+ Given 'I visit profile history page' do
+ visit profile_history_path
+ end
+
Given 'I visit profile token page' do
visit profile_token_path
end
- When 'I visit new project page' do
- visit new_project_path
+ # ----------------------------------------
+ # Admin
+ # ----------------------------------------
+
+ Given 'I visit admin page' do
+ visit admin_root_path
end
+ Given 'I visit admin projects page' do
+ visit admin_projects_path
+ end
+
+ Given 'I visit admin users page' do
+ visit admin_users_path
+ end
+
+ Given 'I visit admin logs page' do
+ visit admin_logs_path
+ end
+
+ Given 'I visit admin hooks page' do
+ visit admin_hooks_path
+ end
+
+ Given 'I visit admin Resque page' do
+ visit admin_resque_path
+ end
+
+ # ----------------------------------------
+ # Generic Project
+ # ----------------------------------------
+
+ Given "I visit my project's home page" do
+ visit project_path(@project)
+ end
+
+ Given "I visit my project's files page" do
+ visit project_tree_path(@project, @project.root_ref)
+ end
+
+ Given "I visit my project's commits page" do
+ visit project_commits_path(@project, @project.root_ref, {limit: 5})
+ end
+
+ Given "I visit my project's network page" do
+ # Stub out find_all to speed this up (10 commits vs. 650)
+ commits = Grit::Commit.find_all(@project.repo, nil, {max_count: 10})
+ Grit::Commit.stub(:find_all).and_return(commits)
+
+ visit graph_project_path(@project)
+ end
+
+ Given "I visit my project's issues page" do
+ visit project_issues_path(@project)
+ end
+
+ Given "I visit my project's merge requests page" do
+ visit project_merge_requests_path(@project)
+ end
+
+ Given "I visit my project's wall page" do
+ visit wall_project_path(@project)
+ end
+
+ Given "I visit my project's wiki page" do
+ visit project_wiki_path(@project, :index)
+ end
+
+ When 'I visit project hooks page' do
+ visit project_hooks_path(@project)
+ end
+
+ # ----------------------------------------
+ # "Shop" Project
+ # ----------------------------------------
+
And 'I visit project "Shop" page' do
project = Project.find_by_name("Shop")
visit project_path(project)
@@ -43,23 +143,27 @@ module SharedPaths
end
Given 'I visit compare refs page' do
- visit compare_project_commits_path(@project)
+ visit project_compare_index_path(@project)
end
Given 'I visit project commits page' do
- visit project_commits_path(@project)
+ visit project_commits_path(@project, @project.root_ref, {limit: 5})
+ end
+
+ Given 'I visit project commits page for stable branch' do
+ visit project_commits_path(@project, 'stable', {limit: 5})
end
Given 'I visit project source page' do
- visit tree_project_ref_path(@project, @project.root_ref)
+ visit project_tree_path(@project, @project.root_ref)
end
Given 'I visit blob file from repo' do
- visit tree_project_ref_path(@project, ValidCommit::ID, :path => ValidCommit::BLOB_FILE_PATH)
+ visit project_tree_path(@project, File.join(ValidCommit::ID, ValidCommit::BLOB_FILE_PATH))
end
Given 'I visit project source page for "8470d70"' do
- visit tree_project_ref_path(@project, "8470d70")
+ visit project_tree_path(@project, "8470d70")
end
Given 'I visit project tags page' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 9b64ca59..ae871d63 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -1,8 +1,19 @@
module SharedProject
include Spinach::DSL
+ # Create a project without caring about what it's called
+ And "I own a project" do
+ @project = create(:project)
+ @project.add_access(@user, :admin)
+ end
+
+ # Create a specific project called "Shop"
And 'I own project "Shop"' do
@project = Factory :project, :name => "Shop"
@project.add_access(@user, :admin)
end
+
+ def current_project
+ @project ||= Project.first
+ end
end
diff --git a/features/support/env.rb b/features/support/env.rb
index 9c6cef07..6d49c25a 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -23,5 +23,7 @@ Spinach.hooks.after_scenario { DatabaseCleaner.clean }
Spinach.hooks.before_run do
RSpec::Mocks::setup self
+ include FactoryGirl::Syntax::Methods
+
stub_gitolite!
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 14390545..a339ec4a 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,7 +1,7 @@
module Gitlab
module APIHelpers
def current_user
- @current_user ||= User.find_by_authentication_token(params[:private_token])
+ @current_user ||= User.find_by_authentication_token(params[:private_token] || env["HTTP_PRIVATE_TOKEN"])
end
def user_project
@@ -22,6 +22,10 @@ module Gitlab
unauthorized! unless current_user
end
+ def authenticated_as_admin!
+ forbidden! unless current_user.is_admin?
+ end
+
def authorize! action, subject
unless abilities.allowed?(current_user, action, subject)
forbidden!
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index c3dc3da6..8f094e0c 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -147,6 +147,19 @@ module Gitlab
@hooks = paginate user_project.hooks
present @hooks, with: Entities::Hook
end
+
+ # Get a project hook
+ #
+ # Parameters:
+ # id (required) - The ID or code name of a project
+ # hook_id (required) - The ID of a project hook
+ # Example Request:
+ # GET /projects/:id/hooks/:hook_id
+ get ":id/hooks/:hook_id" do
+ @hook = user_project.hooks.find(params[:hook_id])
+ present @hook, with: Entities::Hook
+ end
+
# Add hook to project
#
@@ -164,6 +177,27 @@ module Gitlab
error!({'message' => '404 Not found'}, 404)
end
end
+
+ # Update an existing project hook
+ #
+ # Parameters:
+ # id (required) - The ID or code name of a project
+ # hook_id (required) - The ID of a project hook
+ # url (required) - The hook URL
+ # Example Request:
+ # PUT /projects/:id/hooks/:hook_id
+ put ":id/hooks/:hook_id" do
+ @hook = user_project.hooks.find(params[:hook_id])
+ authorize! :admin_project, user_project
+
+ attrs = attributes_for_keys [:url]
+
+ if @hook.update_attributes attrs
+ present @hook, with: Entities::Hook
+ else
+ not_found!
+ end
+ end
# Delete project hook
#
@@ -176,7 +210,6 @@ module Gitlab
authorize! :admin_project, user_project
@hook = user_project.hooks.find(params[:hook_id])
@hook.destroy
- nil
end
# Get a project repository branches
@@ -229,6 +262,16 @@ module Gitlab
present CommitDecorator.decorate(commits), with: Entities::RepoCommit
end
+ # Get a project snippets
+ #
+ # Parameters:
+ # id (required) - The ID or code name of a project
+ # Example Request:
+ # GET /projects/:id/snippets
+ get ":id/snippets" do
+ present paginate(user_project.snippets), with: Entities::ProjectSnippet
+ end
+
# Get a project snippet
#
# Parameters:
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 0ca8fb2a..7f548aaa 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -23,6 +23,30 @@ module Gitlab
@user = User.find(params[:id])
present @user, with: Entities::User
end
+
+ # Create user. Available only for admin
+ #
+ # Parameters:
+ # email (required) - Email
+ # name (required) - Name
+ # password (required) - Password
+ # password_confirmation (required) - Password confirmation
+ # skype - Skype ID
+ # linkedin - Linkedin
+ # twitter - Twitter account
+ # projects_limit - Limit projects wich user can create
+ # Example Request:
+ # POST /users
+ post do
+ authenticated_as_admin!
+ attrs = attributes_for_keys [:email, :name, :password, :password_confirmation, :skype, :linkedin, :twitter, :projects_limit]
+ user = User.new attrs
+ if user.save
+ present user, with: Entities::User
+ else
+ not_found!
+ end
+ end
end
resource :user do
@@ -78,6 +102,8 @@ module Gitlab
key = current_user.keys.find params[:id]
key.delete
end
+
+
end
end
end
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
new file mode 100644
index 00000000..b60dfd03
--- /dev/null
+++ b/lib/extracts_path.rb
@@ -0,0 +1,114 @@
+# Module providing methods for dealing with separating a tree-ish string and a
+# file path string when combined in a request parameter
+module ExtractsPath
+ extend ActiveSupport::Concern
+
+ # Raised when given an invalid file path
+ class InvalidPathError < StandardError; end
+
+ included do
+ if respond_to?(:before_filter)
+ before_filter :assign_ref_vars, only: [:show]
+ end
+ end
+
+ # Given a string containing both a Git tree-ish, such as a branch or tag, and
+ # a filesystem path joined by forward slashes, attempts to separate the two.
+ #
+ # Expects a @project instance variable to contain the active project. This is
+ # used to check the input against a list of valid repository refs.
+ #
+ # Examples
+ #
+ # # No @project available
+ # extract_ref('master')
+ # # => ['', '']
+ #
+ # extract_ref('master')
+ # # => ['master', '']
+ #
+ # extract_ref("f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG")
+ # # => ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG']
+ #
+ # extract_ref("v2.0.0/README.md")
+ # # => ['v2.0.0', 'README.md']
+ #
+ # extract_ref('issues/1234/app/models/project.rb')
+ # # => ['issues/1234', 'app/models/project.rb']
+ #
+ # # Given an invalid branch, we fall back to just splitting on the first slash
+ # extract_ref('non/existent/branch/README.md')
+ # # => ['non', 'existent/branch/README.md']
+ #
+ # Returns an Array where the first value is the tree-ish and the second is the
+ # path
+ def extract_ref(input)
+ pair = ['', '']
+
+ return pair unless @project
+
+ if input.match(/^([[:alnum:]]{40})(.+)/)
+ # If the ref appears to be a SHA, we're done, just split the string
+ pair = $~.captures
+ else
+ # Otherwise, attempt to detect the ref using a list of the project's
+ # branches and tags
+
+ # Append a trailing slash if we only get a ref and no file path
+ id = input
+ id += '/' unless id.ends_with?('/')
+
+ valid_refs = @project.ref_names
+ valid_refs.select! { |v| id.start_with?("#{v}/") }
+
+ if valid_refs.length != 1
+ # No exact ref match, so just try our best
+ pair = id.match(/([^\/]+)(.*)/).captures
+ else
+ # Partition the string into the ref and the path, ignoring the empty first value
+ pair = id.partition(valid_refs.first)[1..-1]
+ end
+ end
+
+ # Remove ending slashes from path
+ pair[1].gsub!(/^\/|\/$/, '')
+
+ pair
+ end
+
+ # Assigns common instance variables for views working with Git tree-ish objects
+ #
+ # Assignments are:
+ #
+ # - @id - A string representing the joined ref and path
+ # - @ref - A string representing the ref (e.g., the branch, tag, or commit SHA)
+ # - @path - A string representing the filesystem path
+ # - @commit - A CommitDecorator representing the commit from the given ref
+ # - @tree - A TreeDecorator representing the tree at the given ref/path
+ #
+ # If the :id parameter appears to be requesting a specific response format,
+ # that will be handled as well.
+ #
+ # Automatically renders `not_found!` if a valid tree path could not be
+ # resolved (e.g., when a user inserts an invalid path or ref).
+ def assign_ref_vars
+ # Handle formats embedded in the id
+ if params[:id].ends_with?('.atom')
+ params[:id].gsub!(/\.atom$/, '')
+ request.format = :atom
+ end
+
+ @ref, @path = extract_ref(params[:id])
+
+ @id = File.join(@ref, @path)
+
+ @commit = CommitDecorator.decorate(@project.commit(@ref))
+
+ @tree = Tree.new(@commit.tree, @project, @ref, @path)
+ @tree = TreeDecorator.new(@tree)
+
+ raise InvalidPathError if @tree.invalid?
+ rescue NoMethodError, InvalidPathError
+ not_found!
+ end
+end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 90bd5d74..5a24c5d0 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -30,7 +30,7 @@ module Gitlab
log.info "#{ldap_prefix}Creating user from #{provider} login"\
" {uid => #{uid}, name => #{name}, email => #{email}}"
password = Devise.friendly_token[0, 8].downcase
- @user = User.new(
+ @user = User.new({
extern_uid: uid,
provider: provider,
name: name,
@@ -38,7 +38,7 @@ module Gitlab
password: password,
password_confirmation: password,
projects_limit: Gitlab.config.default_projects_limit,
- )
+ }, as: :admin)
if Gitlab.config.omniauth['block_auto_created_users'] && !ldap
@user.blocked = true
end
@@ -48,9 +48,13 @@ module Gitlab
def find_or_new_for_omniauth(auth)
provider, uid = auth.provider, auth.uid
+ email = auth.info.email.downcase unless auth.info.email.nil?
if @user = User.find_by_provider_and_extern_uid(provider, uid)
@user
+ elsif @user = User.find_by_email(email)
+ @user.update_attributes(:extern_uid => uid, :provider => provider)
+ @user
else
if Gitlab.config.omniauth['allow_single_sign_on']
@user = create_from_omniauth(auth)
diff --git a/lib/gitlab/backend/gitolite_config.rb b/lib/gitlab/backend/gitolite_config.rb
index f51e8efc..d988164d 100644
--- a/lib/gitlab/backend/gitolite_config.rb
+++ b/lib/gitlab/backend/gitolite_config.rb
@@ -10,7 +10,7 @@ module Gitlab
attr_reader :config_tmp_dir, :ga_repo, :conf
def config_tmp_dir
- @config_tmp_dir ||= File.join(Rails.root, 'tmp',"gitlabhq-gitolite-#{Time.now.to_i}")
+ @config_tmp_dir ||= Rails.root.join('tmp',"gitlabhq-gitolite-#{Time.now.to_i}")
end
def ga_repo
@@ -19,7 +19,7 @@ module Gitlab
def apply
Timeout::timeout(30) do
- File.open(File.join(Rails.root, 'tmp', "gitlabhq-gitolite.lock"), "w+") do |f|
+ File.open(Rails.root.join('tmp', "gitlabhq-gitolite.lock"), "w+") do |f|
begin
# Set exclusive lock
# to prevent race condition
@@ -40,18 +40,22 @@ module Gitlab
# Save changes in
# gitolite-admin repo
- # before pusht it
+ # before push it
ga_repo.save
# Push gitolite-admin repo
# to apply all changes
push(config_tmp_dir)
-
- # Remove tmp dir
- # wiith gitolite-admin
- FileUtils.rm_rf(config_tmp_dir)
ensure
- # unlock so other task cann access
+ # Remove tmp dir
+ # removing the gitolite folder first is important to avoid
+ # NFS issues.
+ FileUtils.rm_rf(File.join(config_tmp_dir, 'gitolite'))
+
+ # Remove parent tmp dir
+ FileUtils.rm_rf(config_tmp_dir)
+
+ # Unlock so other task can access
# gitolite configuration
f.flock(File::LOCK_UN)
end
@@ -92,8 +96,9 @@ module Gitlab
end
def rm_key(user)
- File.unlink(File.join(config_tmp_dir, 'gitolite/keydir',"#{user}.pub"))
- `cd #{File.join(config_tmp_dir,'gitolite')} ; git rm keydir/#{user}.pub`
+ key_path = File.join(config_tmp_dir, 'gitolite/keydir', "#{user}.pub")
+ ga_key = ::Gitolite::SSHKey.from_file(key_path)
+ ga_repo.rm_key(ga_key)
end
# update or create
@@ -146,6 +151,9 @@ module Gitlab
repo.add_permission("RW+", "", name_writers) unless name_writers.blank?
repo.add_permission("RW+", "", name_masters) unless name_masters.blank?
+ # Add sharedRepository config
+ repo.set_git_config("core.sharedRepository", "0660")
+
repo
end
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index 43a75cc3..05329baf 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -1,10 +1,11 @@
module Grack
class Auth < Rack::Auth::Basic
+ attr_accessor :user, :project
def valid?
# Authentication with username and password
email, password = @auth.credentials
- user = User.find_by_email(email)
+ self.user = User.find_by_email(email)
return false unless user.try(:valid_password?, password)
# Set GL_USER env variable
@@ -18,28 +19,39 @@ module Grack
# Find project by PATH_INFO from env
if m = /^\/([\w-]+).git/.match(@request.path_info).to_a
- return false unless project = Project.find_by_path(m.last)
+ self.project = Project.find_by_path(m.last)
+ return false unless project
end
# Git upload and receive
if @request.get?
- true
+ validate_get_request
elsif @request.post?
- if @request.path_info.end_with?('git-upload-pack')
- return project.dev_access_for?(user)
- elsif @request.path_info.end_with?('git-receive-pack')
- if project.protected_branches.map(&:name).include?(current_ref)
- project.master_access_for?(user)
- else
- project.dev_access_for?(user)
- end
- else
- false
- end
+ validate_post_request
else
false
end
- end# valid?
+ end
+
+ def validate_get_request
+ true
+ end
+
+ def validate_post_request
+ if @request.path_info.end_with?('git-upload-pack')
+ can?(user, :push_code, project)
+ elsif @request.path_info.end_with?('git-receive-pack')
+ action = if project.protected_branch?(current_ref)
+ :push_code_to_protected_branches
+ else
+ :push_code
+ end
+
+ can?(user, action, project)
+ else
+ false
+ end
+ end
def current_ref
if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/
diff --git a/lib/gitlab/file_editor.rb b/lib/gitlab/file_editor.rb
new file mode 100644
index 00000000..dc3f9480
--- /dev/null
+++ b/lib/gitlab/file_editor.rb
@@ -0,0 +1,58 @@
+module Gitlab
+ # GitLab file editor
+ #
+ # It gives you ability to make changes to files
+ # & commit this changes from GitLab UI.
+ class FileEditor
+ attr_accessor :user, :project, :ref
+
+ def initialize(user, project, ref)
+ self.user = user
+ self.project = project
+ self.ref = ref
+ end
+
+ def update(path, content, commit_message, last_commit)
+ return false unless can_edit?(path, last_commit)
+
+ Grit::Git.with_timeout(10.seconds) do
+ lock_file = Rails.root.join("tmp", "#{project.path}.lock")
+
+ File.open(lock_file, "w+") do |f|
+ f.flock(File::LOCK_EX)
+
+ unless project.satellite.exists?
+ raise "Satellite doesn't exist"
+ end
+
+ project.satellite.clear
+
+ Dir.chdir(project.satellite.path) do
+ r = Grit::Repo.new('.')
+ r.git.sh "git reset --hard"
+ r.git.sh "git fetch origin"
+ r.git.sh "git config user.name \"#{user.name}\""
+ r.git.sh "git config user.email \"#{user.email}\""
+ r.git.sh "git checkout -b #{ref} origin/#{ref}"
+ File.open(path, 'w'){|f| f.write(content)}
+ r.git.sh "git add ."
+ r.git.sh "git commit -am '#{commit_message}'"
+ output = r.git.sh "git push origin #{ref}"
+
+ if output =~ /reject/
+ return false
+ end
+ end
+ end
+ end
+ true
+ end
+
+ protected
+
+ def can_edit?(path, last_commit)
+ current_last_commit = @project.last_commit_for(ref, path).sha
+ last_commit == current_last_commit
+ end
+ end
+end
diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb
index 0f289a61..7a0a3214 100644
--- a/lib/gitlab/inline_diff.rb
+++ b/lib/gitlab/inline_diff.rb
@@ -21,14 +21,13 @@ module Gitlab
end
end
first_token = first_line[0..first_the_same_symbols][1..-1]
-
diff_arr[index+1].sub!(first_token, first_token + START)
diff_arr[index+2].sub!(first_token, first_token + START)
-
last_the_same_symbols = 0
(1..max_length + 1).each do |i|
last_the_same_symbols = -i
- if first_line[-i] != second_line[-i]
+ shortest_line = second_line.size > first_line.size ? first_line : second_line
+ if ( first_line[-i] != second_line[-i] ) || "#{first_token}#{START}".size == shortest_line[1..-i].size
break
end
end
diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb
index 9405163d..cf9a4c4a 100644
--- a/lib/gitlab/logger.rb
+++ b/lib/gitlab/logger.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def self.build
- new(File.join(Rails.root, "log", file_name))
+ new(Rails.root.join("log", file_name))
end
end
end
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index 9201003e..ee0ee05c 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -128,7 +128,7 @@ module Gitlab
#
# Returns boolean
def valid_emoji?(emoji)
- File.exists?(Rails.root.join('app', 'assets', 'images', 'emoji', "#{emoji}.png"))
+ Emoji.names.include? emoji
end
# Private: Dispatches to a dedicated processing method based on reference
@@ -173,8 +173,8 @@ module Gitlab
end
def reference_commit(identifier)
- if commit = @project.commit(identifier)
- link_to(identifier, project_commit_path(@project, id: commit.id), html_options.merge(title: CommitDecorator.new(commit).link_title, class: "gfm gfm-commit #{html_options[:class]}"))
+ if @project.valid_repo? && commit = @project.commit(identifier)
+ link_to(identifier, project_commit_path(@project, commit), html_options.merge(title: CommitDecorator.new(commit).link_title, class: "gfm gfm-commit #{html_options[:class]}"))
end
end
end
diff --git a/lib/gitlab/merge.rb b/lib/gitlab/merge.rb
index 18013574..bf7aaa51 100644
--- a/lib/gitlab/merge.rb
+++ b/lib/gitlab/merge.rb
@@ -1,65 +1,106 @@
module Gitlab
class Merge
- attr_accessor :project, :merge_request, :user
+ attr_accessor :merge_request, :project, :user
def initialize(merge_request, user)
- self.user = user
- self.merge_request = merge_request
- self.project = merge_request.project
+ @merge_request = merge_request
+ @project = merge_request.project
+ @user = user
end
def can_be_merged?
- result = false
- process do |repo, output|
- result = !(output =~ /CONFLICT/)
+ in_locked_and_timed_satellite do |merge_repo|
+ merge_in_satellite!(merge_repo)
end
- result
end
- def merge
- process do |repo, output|
- if output =~ /CONFLICT/
- false
- else
- !!repo.git.push({}, "origin", merge_request.target_branch)
+ # Merges the source branch into the target branch in the satellite and
+ # pushes it back to Gitolite.
+ # It also removes the source branch if requested in the merge request.
+ #
+ # Returns false if the merge produced conflicts
+ # Returns false if pushing from the satallite to Gitolite failed or was rejected
+ # Returns true otherwise
+ def merge!
+ in_locked_and_timed_satellite do |merge_repo|
+ if merge_in_satellite!(merge_repo)
+ # push merge back to Gitolite
+ # will raise CommandFailed when push fails
+ merge_repo.git.push({raise: true}, :origin, merge_request.target_branch)
+
+ # remove source branch
+ if merge_request.should_remove_source_branch && !project.root_ref?(merge_request.source_branch)
+ # will raise CommandFailed when push fails
+ merge_repo.git.push({raise: true}, :origin, ":#{merge_request.source_branch}")
+ end
+
+ # merge, push and branch removal successful
+ true
end
end
+ rescue Grit::Git::CommandFailed
+ false
end
- def process
+ private
+
+ # * Sets a 30s timeout for Git
+ # * Locks the satellite repo
+ # * Yields the prepared satallite repo
+ def in_locked_and_timed_satellite
Grit::Git.with_timeout(30.seconds) do
- lock_file = File.join(Rails.root, "tmp", "merge_repo_#{project.path}.lock")
+ lock_file = Rails.root.join("tmp", "#{project.path}.lock")
File.open(lock_file, "w+") do |f|
f.flock(File::LOCK_EX)
unless project.satellite.exists?
- raise "You should run: rake gitlab:app:enable_automerge"
+ raise "Satellite doesn't exist"
end
-
- project.satellite.clear
Dir.chdir(project.satellite.path) do
- merge_repo = Grit::Repo.new('.')
- merge_repo.git.sh "git reset --hard"
- merge_repo.git.sh "git fetch origin"
- merge_repo.git.sh "git config user.name \"#{user.name}\""
- merge_repo.git.sh "git config user.email \"#{user.email}\""
- merge_repo.git.sh "git checkout -b #{merge_request.target_branch} origin/#{merge_request.target_branch}"
- output = merge_repo.git.pull({}, "--no-ff", "origin", merge_request.source_branch)
+ repo = Grit::Repo.new('.')
- #remove source-branch
- if merge_request.should_remove_source_branch && !project.root_ref?(merge_request.source_branch)
- merge_repo.git.sh "git push origin :#{merge_request.source_branch}"
- end
-
- yield(merge_repo, output)
+ return yield repo
end
end
end
-
+ rescue Errno::ENOMEM => ex
+ Gitlab::GitLogger.error(ex.message)
rescue Grit::Git::GitTimeout
return false
end
+
+ # Merges the source_branch into the target_branch in the satellite.
+ #
+ # Note: it will clear out the satellite before doing anything
+ #
+ # Returns false if the merge produced conflicts
+ # Returns true otherwise
+ def merge_in_satellite!(repo)
+ prepare_satelite!(repo)
+
+ # create target branch in satellite at the corresponding commit from Gitolite
+ repo.git.checkout({b: true}, merge_request.target_branch, "origin/#{merge_request.target_branch}")
+
+ # merge the source branch from Gitolite into the satellite
+ # will raise CommandFailed when merge fails
+ repo.git.pull({no_ff: true, raise: true}, :origin, merge_request.source_branch)
+ rescue Grit::Git::CommandFailed
+ false
+ end
+
+ # * Clears the satellite
+ # * Updates the satellite from Gitolite
+ # * Sets up Git variables for the user
+ def prepare_satelite!(repo)
+ project.satellite.clear
+
+ repo.git.reset(hard: true)
+ repo.git.fetch({}, :origin)
+
+ repo.git.config({}, "user.name", user.name)
+ repo.git.config({}, "user.email", user.email)
+ end
end
end
diff --git a/lib/gitlab/satellite.rb b/lib/gitlab/satellite.rb
index 4bcbfe8d..9d8dfb8e 100644
--- a/lib/gitlab/satellite.rb
+++ b/lib/gitlab/satellite.rb
@@ -1,6 +1,6 @@
module Gitlab
class Satellite
-
+
PARKING_BRANCH = "__parking_branch"
attr_accessor :project
@@ -14,7 +14,7 @@ module Gitlab
end
def path
- File.join(Rails.root, "tmp", "repo_satellites", project.path)
+ Rails.root.join("tmp", "repo_satellites", project.path)
end
def exists?
@@ -36,6 +36,6 @@ module Gitlab
end
end
end
-
+
end
end
diff --git a/lib/hooks/post-receive b/lib/hooks/post-receive
index a4fa9f1c..4e596679 100755
--- a/lib/hooks/post-receive
+++ b/lib/hooks/post-receive
@@ -7,6 +7,6 @@ while read oldrev newrev ref
do
# For every branch or tag that was pushed, create a Resque job in redis.
pwd=`pwd`
- reponame=`basename "$pwd" | cut -d. -f1`
+ reponame=`basename "$pwd" | sed s/\.git$//`
env -i redis-cli rpush "resque:queue:post_receive" "{\"class\":\"PostReceive\",\"args\":[\"$reponame\",\"$oldrev\",\"$newrev\",\"$ref\",\"$GL_USER\"]}" > /dev/null 2>&1
done
diff --git a/lib/tasks/bulk_add_permission.rake b/lib/tasks/bulk_add_permission.rake
index 55797825..bf08ace8 100644
--- a/lib/tasks/bulk_add_permission.rake
+++ b/lib/tasks/bulk_add_permission.rake
@@ -1,26 +1,20 @@
-desc "Add all users to all projects, system administratos are added as masters"
+desc "Add all users to all projects (admin users are added as masters)"
task :add_users_to_project_teams => :environment do |t, args|
- users = User.find_all_by_admin(false, :select => 'id').map(&:id)
- admins = User.find_all_by_admin(true, :select => 'id').map(&:id)
+ user_ids = User.where(:admin => false).pluck(:id)
+ admin_ids = User.where(:admin => true).pluck(:id)
- users.each do |user|
- puts "#{user}"
- end
-
- Project.all.each do |project|
- puts "Importing #{users.length} users into #{project.path}"
- UsersProject.bulk_import(project, users, UsersProject::DEVELOPER)
- puts "Importing #{admins.length} admins into #{project.path}"
- UsersProject.bulk_import(project, admins, UsersProject::MASTER)
+ Project.find_each do |project|
+ puts "Importing #{user_ids.size} users into #{project.code}"
+ UsersProject.bulk_import(project, user_ids, UsersProject::DEVELOPER)
+ puts "Importing #{admin_ids.size} admins into #{project.code}"
+ UsersProject.bulk_import(project, admin_ids, UsersProject::MASTER)
end
end
desc "Add user to as a developer to all projects"
task :add_user_to_project_teams, [:email] => :environment do |t, args|
- user_email = args.email
- user = User.find_by_email(user_email)
+ user = User.find_by_email args.email
+ project_ids = Project.pluck(:id)
- project_ids = Project.all.map(&:id)
-
- UsersProject.user_bulk_import(user,project_ids,UsersProject::DEVELOPER)
+ UsersProject.user_bulk_import(user, project_ids, UsersProject::DEVELOPER)
end
diff --git a/lib/tasks/bulk_import.rake b/lib/tasks/bulk_import.rake
index edb4a599..914f920a 100644
--- a/lib/tasks/bulk_import.rake
+++ b/lib/tasks/bulk_import.rake
@@ -1,20 +1,17 @@
-
desc "Imports existing Git repos from a directory into new projects in git_base_path"
task :import_projects, [:directory,:email] => :environment do |t, args|
- user_email = args.email
- import_directory = args.directory
+ user_email, import_directory = args.email, args.directory
repos_to_import = Dir.glob("#{import_directory}/*")
git_base_path = Gitlab.config.git_base_path
- puts "Found #{repos_to_import.length} repos to import"
+ imported_count, skipped_count, failed_count = 0
+
+ puts "Found #{repos_to_import.size} repos to import"
- imported_count = 0
- skipped_count = 0
- failed_count = 0
repos_to_import.each do |repo_path|
repo_name = File.basename repo_path
+ clone_path = "#{git_base_path}#{repo_name}.git"
puts " Processing #{repo_name}"
- clone_path = "#{git_base_path}#{repo_name}.git"
if Dir.exists? clone_path
if Project.find_by_code(repo_name)
@@ -38,7 +35,6 @@ task :import_projects, [:directory,:email] => :environment do |t, args|
else
failed_count += 1
end
-
end
puts "Finished importing #{imported_count} projects (skipped #{skipped_count}, failed #{failed_count})."
@@ -49,63 +45,39 @@ def clone_bare_repo_as_git(existing_path, new_path)
git_user = Gitlab.config.ssh_user
begin
sh "sudo -u #{git_user} -i git clone --bare '#{existing_path}' #{new_path}"
- true
- rescue Exception=> msg
- puts " ERROR: Faild to clone #{existing_path} to #{new_path}"
- puts " Make sure #{git_user} can reach #{existing_path}"
- puts " Exception-MSG: #{msg}"
- false
+ rescue Exception => msg
+ puts " ERROR: Failed to clone #{existing_path} to #{new_path}"
+ puts " Make sure #{git_user} can reach #{existing_path}"
+ puts " Exception-MSG: #{msg}"
end
end
-# Creats a project in Gitlag given a @project_name@ to use (for name, web url, and code
-# url) and a @user_email@ that will be assigned as the owner of the project.
+# Creates a project in GitLab given a `project_name` to use
+# (for name, web url, and code url) and a `user_email` that will be
+# assigned as the owner of the project.
def create_repo_project(project_name, user_email)
- user = User.find_by_email(user_email)
- if user
+ if user = User.find_by_email(user_email)
# Using find_by_code since that's the most important identifer to be unique
if Project.find_by_code(project_name)
puts " INFO: Project #{project_name} already exists in Gitlab, skipping."
- false
else
- project = nil
- if Project.find_by_code(project_name)
- puts " ERROR: Project already exists #{project_name}"
- return false
- project = Project.find_by_code(project_name)
- else
- project = Project.create(
- name: project_name,
- code: project_name,
- path: project_name,
- owner: user,
- description: "Automatically created from Rake on #{Time.now.to_s}"
- )
- end
-
- unless project.valid?
- puts " ERROR: Failed to create project #{project} because #{project.errors.first}"
- return false
- end
-
- # Add user as admin for project
- project.users_projects.create!(
- :project_access => UsersProject::MASTER,
- :user => user
+ project = Project.create(
+ name: project_name,
+ code: project_name,
+ path: project_name,
+ owner: user,
+ description: "Automatically created from 'import_projects' rake task on #{Time.now}"
)
- # Per projects_controller.rb#37
- project.update_repository
-
if project.valid?
- true
+ # Add user as admin for project
+ project.users_projects.create!(:project_access => UsersProject::MASTER, :user => user)
+ project.update_repository
else
puts " ERROR: Failed to create project #{project} because #{project.errors.first}"
- false
end
end
else
- puts " ERROR: #{user_email} not found, skipping"
- false
+ puts " ERROR: user with #{user_email} not found, skipping"
end
end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 04d240f6..06d7cb65 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -2,22 +2,20 @@ require 'active_record/fixtures'
namespace :gitlab do
namespace :app do
-
- # Create backup of gitlab system
- desc "GITLAB | Create a backup of the gitlab system"
+ # Create backup of GitLab system
+ desc "GITLAB | Create a backup of the GitLab system"
task :backup_create => :environment do
-
Rake::Task["gitlab:app:db_dump"].invoke
Rake::Task["gitlab:app:repo_dump"].invoke
Dir.chdir(Gitlab.config.backup_path)
# saving additional informations
- s = Hash.new
- s["db_version"] = "#{ActiveRecord::Migrator.current_version}"
- s["backup_created_at"] = "#{Time.now}"
- s["gitlab_version"] = %x{git rev-parse HEAD}.gsub(/\n/,"")
- s["tar_version"] = %x{tar --version | head -1}.gsub(/\n/,"")
+ s = {}
+ s[:db_version] = "#{ActiveRecord::Migrator.current_version}"
+ s[:backup_created_at] = "#{Time.now}"
+ s[:gitlab_version] = %x{git rev-parse HEAD}.gsub(/\n/,"")
+ s[:tar_version] = %x{tar --version | head -1}.gsub(/\n/,"")
File.open("#{Gitlab.config.backup_path}/backup_information.yml", "w+") do |file|
file << s.to_yaml.gsub(/^---\n/,'')
@@ -32,7 +30,7 @@ namespace :gitlab do
end
# cleanup: remove tmp files
- print "Deletion of tmp directories..."
+ print "Deleting tmp directories..."
if Kernel.system("rm -rf repositories/ db/ backup_information.yml")
puts "[DONE]".green
else
@@ -52,26 +50,23 @@ namespace :gitlab do
else
puts "[SKIPPING]".yellow
end
-
end
-
- # Restore backup of gitlab system
+ # Restore backup of GitLab system
desc "GITLAB | Restore a previously created backup"
task :backup_restore => :environment do
-
Dir.chdir(Gitlab.config.backup_path)
# check for existing backups in the backup dir
file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i }
- puts "no backup found" if file_list.count == 0
+ puts "no backups found" if file_list.count == 0
if file_list.count > 1 && ENV["BACKUP"].nil?
puts "Found more than one backup, please specify which one you want to restore:"
puts "rake gitlab:app:backup_restore BACKUP=timestamp_of_backup"
exit 1;
end
- tar_file = ENV["BACKUP"].nil? ? File.join(file_list.first.to_s + "_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar")
+ tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar")
unless File.exists?(tar_file)
puts "The specified backup doesn't exist!"
@@ -102,16 +97,14 @@ namespace :gitlab do
Rake::Task["gitlab:app:repo_restore"].invoke
# cleanup: remove tmp files
- print "Deletion of tmp directories..."
+ print "Deleting tmp directories..."
if Kernel.system("rm -rf repositories/ db/ backup_information.yml")
puts "[DONE]".green
else
puts "[FAILED]".red
end
-
end
-
################################################################################
################################# invoked tasks ################################
@@ -121,7 +114,7 @@ namespace :gitlab do
backup_path_repo = File.join(Gitlab.config.backup_path, "repositories")
FileUtils.mkdir_p(backup_path_repo) until Dir.exists?(backup_path_repo)
puts "Dumping repositories:"
- project = Project.all.map { |n| [n.path,n.path_to_repo] }
+ project = Project.all.map { |n| [n.path, n.path_to_repo] }
project << ["gitolite-admin.git", File.join(File.dirname(project.first.second), "gitolite-admin.git")]
project.each do |project|
print "- Dumping repository #{project.first}... "
@@ -136,11 +129,11 @@ namespace :gitlab do
task :repo_restore => :environment do
backup_path_repo = File.join(Gitlab.config.backup_path, "repositories")
puts "Restoring repositories:"
- project = Project.all.map { |n| [n.path,n.path_to_repo] }
+ project = Project.all.map { |n| [n.path, n.path_to_repo] }
project << ["gitolite-admin.git", File.join(File.dirname(project.first.second), "gitolite-admin.git")]
project.each do |project|
print "- Restoring repository #{project.first}... "
- FileUtils.rm_rf(project.second) if File.dirname(project.second) # delet old stuff
+ FileUtils.rm_rf(project.second) if File.dirname(project.second) # delete old stuff
if Kernel.system("cd #{File.dirname(project.second)} > /dev/null 2>&1 && git clone --bare #{backup_path_repo}/#{project.first}.bundle #{project.first}.git > /dev/null 2>&1")
permission_commands = [
"sudo chmod -R g+rwX #{Gitlab.config.git_base_path}",
@@ -157,8 +150,9 @@ namespace :gitlab do
###################################### DB ######################################
task :db_dump => :environment do
- backup_path_db = File.join(Gitlab.config.backup_path, "db")
- FileUtils.mkdir_p(backup_path_db) until Dir.exists?(backup_path_db)
+ backup_path_db = File.join(Gitlab.config.backup_path, "db")
+ FileUtils.mkdir_p(backup_path_db) unless Dir.exists?(backup_path_db)
+
puts "Dumping database tables:"
ActiveRecord::Base.connection.tables.each do |tbl|
print "- Dumping table #{tbl}... "
@@ -176,9 +170,11 @@ namespace :gitlab do
end
task :db_restore=> :environment do
- backup_path_db = File.join(Gitlab.config.backup_path, "db")
+ backup_path_db = File.join(Gitlab.config.backup_path, "db")
+
puts "Restoring database tables:"
Rake::Task["db:reset"].invoke
+
Dir.glob(File.join(backup_path_db, "*.yml") ).each do |dir|
fixture_file = File.basename(dir, ".*" )
print "- Loading fixture #{fixture_file}..."
diff --git a/lib/tasks/gitlab/enable_automerge.rake b/lib/tasks/gitlab/enable_automerge.rake
index 0a1a0fa7..13b4bab6 100644
--- a/lib/tasks/gitlab/enable_automerge.rake
+++ b/lib/tasks/gitlab/enable_automerge.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :app do
desc "GITLAB | Enable auto merge"
- task :enable_automerge => :environment do
+ task :enable_automerge => :environment do
Gitlab::Gitolite.new.enable_automerge
Project.find_each do |project|
diff --git a/lib/tasks/gitlab/gitolite_rebuild.rake b/lib/tasks/gitlab/gitolite_rebuild.rake
index 534aa315..fce10eb5 100644
--- a/lib/tasks/gitlab/gitolite_rebuild.rake
+++ b/lib/tasks/gitlab/gitolite_rebuild.rake
@@ -1,11 +1,10 @@
namespace :gitlab do
namespace :gitolite do
desc "GITLAB | Rebuild each project at gitolite config"
- task :update_repos => :environment do
+ task :update_repos => :environment do
puts "Starting Projects"
Project.find_each(:batch_size => 100) do |project|
- puts
- puts "=== #{project.name}"
+ puts "\n=== #{project.name}"
project.update_repository
puts
end
diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake
index 49c86461..08f35c7e 100644
--- a/lib/tasks/gitlab/setup.rake
+++ b/lib/tasks/gitlab/setup.rake
@@ -4,8 +4,7 @@ namespace :gitlab do
task :setup => [
'db:setup',
'db:seed_fu',
- 'gitlab:app:enable_automerge'
+ 'gitlab:app:enable_automerge'
]
end
end
-
diff --git a/lib/tasks/gitlab/status.rake b/lib/tasks/gitlab/status.rake
index e5b5e122..302f417c 100644
--- a/lib/tasks/gitlab/status.rake
+++ b/lib/tasks/gitlab/status.rake
@@ -1,37 +1,37 @@
namespace :gitlab do
namespace :app do
- desc "GITLAB | Check gitlab installation status"
+ desc "GITLAB | Check GitLab installation status"
task :status => :environment do
- puts "Starting diagnostic".yellow
+ puts "Starting diagnostics".yellow
git_base_path = Gitlab.config.git_base_path
print "config/database.yml............"
- if File.exists?(File.join Rails.root, "config", "database.yml")
+ if File.exists?(Rails.root.join "config", "database.yml")
puts "exists".green
- else
+ else
puts "missing".red
return
end
print "config/gitlab.yml............"
- if File.exists?(File.join Rails.root, "config", "gitlab.yml")
- puts "exists".green
+ if File.exists?(Rails.root.join "config", "gitlab.yml")
+ puts "exists".green
else
puts "missing".red
return
end
print "#{git_base_path}............"
- if File.exists?(git_base_path)
- puts "exists".green
- else
+ if File.exists?(git_base_path)
+ puts "exists".green
+ else
puts "missing".red
return
end
print "#{git_base_path} is writable?............"
if File.stat(git_base_path).writable?
- puts "YES".green
+ puts "YES".green
else
puts "NO".red
return
@@ -41,16 +41,16 @@ namespace :gitlab do
`git clone #{Gitlab.config.gitolite_admin_uri} /tmp/gitolite_gitlab_test`
FileUtils.rm_rf("/tmp/gitolite_gitlab_test")
print "Can clone gitolite-admin?............"
- puts "YES".green
- rescue
+ puts "YES".green
+ rescue
print "Can clone gitolite-admin?............"
puts "NO".red
return
end
print "UMASK for .gitolite.rc is 0007? ............"
- unless open("#{git_base_path}/../.gitolite.rc").grep(/UMASK([ \t]*)=([ \t>]*)0007/).empty?
- puts "YES".green
+ if open("#{git_base_path}/../.gitolite.rc").grep(/UMASK([ \t]*)=([ \t>]*)0007/).any?
+ puts "YES".green
else
puts "NO".red
return
@@ -69,16 +69,15 @@ namespace :gitlab do
end
end
-
- if Project.count > 0
+ if Project.count > 0
puts "Validating projects repositories:".yellow
Project.find_each(:batch_size => 100) do |project|
print "#{project.name}....."
- hook_file = File.join(project.path_to_repo, 'hooks','post-receive')
+ hook_file = File.join(project.path_to_repo, 'hooks', 'post-receive')
unless File.exists?(hook_file)
- puts "post-receive file missing".red
- next
+ puts "post-receive file missing".red
+ return
end
puts "post-receive file ok".green
diff --git a/lib/tasks/gitlab/write_hook.rake b/lib/tasks/gitlab/write_hook.rake
deleted file mode 100644
index 5e9fc8eb..00000000
--- a/lib/tasks/gitlab/write_hook.rake
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace :gitlab do
- namespace :gitolite do
- desc "GITLAB | Write GitLab hook for gitolite"
- task :write_hooks => :environment do
- gitolite_hooks_path = File.join(Gitlab.config.git_hooks_path, "common")
- gitlab_hooks_path = Rails.root.join("lib", "hooks")
-
- gitlab_hook_files = ['post-receive']
-
- gitlab_hook_files.each do |file_name|
- source = File.join(gitlab_hooks_path, file_name)
- dest = File.join(gitolite_hooks_path, file_name)
-
- puts "sudo -u root cp #{source} #{dest}".yellow
- `sudo -u root cp #{source} #{dest}`
-
- puts "sudo -u root chown git:git #{dest}".yellow
- `sudo -u root chown git:git #{dest}`
- end
- end
- end
-end
-
diff --git a/spec/controllers/commits_controller_spec.rb b/spec/controllers/commits_controller_spec.rb
new file mode 100644
index 00000000..bf335634
--- /dev/null
+++ b/spec/controllers/commits_controller_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe CommitsController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ project.add_access(user, :read, :admin)
+ end
+
+ describe "GET show" do
+ context "as atom feed" do
+ it "should render as atom" do
+ get :show, project_id: project.code, id: "master.atom"
+ response.should be_success
+ response.content_type.should == 'application/atom+xml'
+ end
+ end
+ end
+end
diff --git a/spec/controllers/tree_controller_spec.rb b/spec/controllers/tree_controller_spec.rb
new file mode 100644
index 00000000..b9295537
--- /dev/null
+++ b/spec/controllers/tree_controller_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe TreeController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ project.add_access(user, :read, :admin)
+
+ project.stub(:branches).and_return(['master', 'foo/bar/baz'])
+ project.stub(:tags).and_return(['v1.0.0', 'v2.0.0'])
+ controller.instance_variable_set(:@project, project)
+ end
+
+ describe "GET show" do
+ # Make sure any errors accessing the tree in our views bubble up to this spec
+ render_views
+
+ before { get :show, project_id: project.code, id: id }
+
+ context "valid branch, no path" do
+ let(:id) { 'master' }
+ it { should respond_with(:success) }
+ end
+
+ context "valid branch, valid path" do
+ let(:id) { 'master/README.md' }
+ it { should respond_with(:success) }
+ end
+
+ context "valid branch, invalid path" do
+ let(:id) { 'master/invalid-path.rb' }
+ it { should respond_with(:not_found) }
+ end
+
+ context "invalid branch, valid path" do
+ let(:id) { 'invalid-branch/README.md' }
+ it { should respond_with(:not_found) }
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index 92790a3f..0258f892 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -42,8 +42,14 @@ FactoryGirl.define do
factory :project do
sequence(:name) { |n| "project#{n}" }
- path { name }
- code { name }
+ path { name.downcase.gsub(/\s/, '_') }
+ code { name.downcase.gsub(/\s/, '_') }
+ owner
+ end
+
+ factory :group do
+ sequence(:name) { |n| "group#{n}" }
+ code { name.downcase.gsub(/\s/, '_') }
owner
end
@@ -70,6 +76,12 @@ FactoryGirl.define do
project
source_branch "master"
target_branch "stable"
+
+ trait :closed do
+ closed true
+ end
+
+ factory :closed_merge_request, traits: [:closed]
end
factory :note do
@@ -78,16 +90,18 @@ FactoryGirl.define do
end
factory :event do
+ factory :closed_issue_event do
+ project
+ action Event::Closed
+ target factory: :closed_issue
+ author factory: :user
+ end
end
factory :key do
title
key do
- """
- ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4
- 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4
- soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=
- """
+ "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
end
factory :deploy_key do
@@ -97,6 +111,12 @@ FactoryGirl.define do
factory :personal_key do
user
end
+
+ factory :key_with_a_space_in_the_middle do
+ key do
+ "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa ++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
+ end
+ end
end
factory :milestone do
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 5ccc17bd..5ee73546 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -1,6 +1,9 @@
require 'spec_helper'
+INVALID_FACTORIES = [:key_with_a_space_in_the_middle]
+
FactoryGirl.factories.map(&:name).each do |factory_name|
+ next if INVALID_FACTORIES.include?(factory_name)
describe "#{factory_name} factory" do
it 'should be valid' do
build(factory_name).should be_valid
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 9a2df314..a94d5505 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -1,6 +1,44 @@
require 'spec_helper'
describe ApplicationHelper do
+ describe 'current_controller?' do
+ before do
+ controller.stub!(:controller_name).and_return('foo')
+ end
+
+ it "returns true when controller matches argument" do
+ current_controller?(:foo).should be_true
+ end
+
+ it "returns false when controller does not match argument" do
+ current_controller?(:bar).should_not be_true
+ end
+
+ it "should take any number of arguments" do
+ current_controller?(:baz, :bar).should_not be_true
+ current_controller?(:baz, :bar, :foo).should be_true
+ end
+ end
+
+ describe 'current_action?' do
+ before do
+ stub!(:action_name).and_return('foo')
+ end
+
+ it "returns true when action matches argument" do
+ current_action?(:foo).should be_true
+ end
+
+ it "returns false when action does not match argument" do
+ current_action?(:bar).should_not be_true
+ end
+
+ it "should take any number of arguments" do
+ current_action?(:baz, :bar).should_not be_true
+ current_action?(:baz, :bar, :foo).should be_true
+ end
+ end
+
describe "gravatar_icon" do
let(:user_email) { 'user@email.com' }
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index a6708a7a..ec830e40 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -329,9 +329,9 @@ describe GitlabMarkdownHelper do
end
it "should leave code blocks untouched" do
- markdown("\n some code from $#{snippet.id}\n here too\n").should == "
somecodefrom $#{snippet.id}\nheretoo\n
\n
\n"
+ markdown("\n some code from $#{snippet.id}\n here too\n").should == "
somecodefrom $#{snippet.id}\nheretoo\n
"
- markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n").should == "
somecodefrom $#{snippet.id}\nheretoo\n
\n
\n"
+ markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n").should == "
somecodefrom $#{snippet.id}\nheretoo\n
"
end
it "should leave inline code untouched" do
diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb
new file mode 100644
index 00000000..ef8e4cf6
--- /dev/null
+++ b/spec/helpers/tab_helper_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe TabHelper do
+ include ApplicationHelper
+
+ describe 'nav_link' do
+ before do
+ controller.stub!(:controller_name).and_return('foo')
+ stub!(:action_name).and_return('foo')
+ end
+
+ it "captures block output" do
+ nav_link { "Testing Blocks" }.should match(/Testing Blocks/)
+ end
+
+ it "performs checks on the current controller" do
+ nav_link(controller: :foo).should match(/
/)
+ nav_link(controller: :bar).should_not match(/active/)
+ nav_link(controller: [:foo, :bar]).should match(/active/)
+ end
+
+ it "performs checks on the current action" do
+ nav_link(action: :foo).should match(/
/)
+ nav_link(action: :bar).should_not match(/active/)
+ nav_link(action: [:foo, :bar]).should match(/active/)
+ end
+
+ it "performs checks on both controller and action when both are present" do
+ nav_link(controller: :bar, action: :foo).should_not match(/active/)
+ nav_link(controller: :foo, action: :bar).should_not match(/active/)
+ nav_link(controller: :foo, action: :foo).should match(/active/)
+ end
+
+ it "accepts a path shorthand" do
+ nav_link(path: 'foo#bar').should_not match(/active/)
+ nav_link(path: 'foo#foo').should match(/active/)
+ end
+
+ it "passes extra html options to the list element" do
+ nav_link(action: :foo, html_options: {class: 'home'}).should match(/
/)
+ end
+ end
+end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
new file mode 100644
index 00000000..cf422017
--- /dev/null
+++ b/spec/lib/extracts_path_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe ExtractsPath do
+ include ExtractsPath
+
+ let(:project) { double('project') }
+
+ before do
+ @project = project
+ project.stub(:ref_names).and_return(['master', 'foo/bar/baz', 'v1.0.0', 'v2.0.0'])
+ end
+
+ describe '#extract_ref' do
+ it "returns an empty pair when no @project is set" do
+ @project = nil
+ extract_ref('master/CHANGELOG').should == ['', '']
+ end
+
+ context "without a path" do
+ it "extracts a valid branch" do
+ extract_ref('master').should == ['master', '']
+ end
+
+ it "extracts a valid tag" do
+ extract_ref('v2.0.0').should == ['v2.0.0', '']
+ end
+
+ it "extracts a valid commit ref without a path" do
+ extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062').should ==
+ ['f4b14494ef6abf3d144c28e4af0c20143383e062', '']
+ end
+
+ it "falls back to a primitive split for an invalid ref" do
+ extract_ref('stable').should == ['stable', '']
+ end
+ end
+
+ context "with a path" do
+ it "extracts a valid branch" do
+ extract_ref('foo/bar/baz/CHANGELOG').should == ['foo/bar/baz', 'CHANGELOG']
+ end
+
+ it "extracts a valid tag" do
+ extract_ref('v2.0.0/CHANGELOG').should == ['v2.0.0', 'CHANGELOG']
+ end
+
+ it "extracts a valid commit SHA" do
+ extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG').should ==
+ ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG']
+ end
+
+ it "falls back to a primitive split for an invalid ref" do
+ extract_ref('stable/CHANGELOG').should == ['stable', 'CHANGELOG']
+ end
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 4a9f142e..874864a3 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -235,7 +235,7 @@ describe Notify do
commit.stub(:safe_message).and_return('some message')
end
end
- before(:each) { note.stub(:target).and_return(commit) }
+ before(:each) { note.stub(:noteable).and_return(commit) }
subject { Notify.note_commit_email(recipient.id, note.id) }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
new file mode 100644
index 00000000..e4bc1936
--- /dev/null
+++ b/spec/models/commit_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Commit do
+ let(:commit) { create(:project).commit }
+
+ describe CommitDecorator do
+ let(:decorator) { CommitDecorator.new(commit) }
+
+ describe '#title' do
+ it "returns no_commit_message when safe_message is blank" do
+ decorator.stub(:safe_message).and_return('')
+ decorator.title.should == "--no commit message"
+ end
+
+ it "truncates a message without a newline at 70 characters" do
+ message = commit.safe_message * 10
+
+ decorator.stub(:safe_message).and_return(message)
+ decorator.title.should == "#{message[0..69]}…"
+ end
+
+ it "truncates a message with a newline before 80 characters at the newline" do
+ message = commit.safe_message.split(" ").first
+
+ decorator.stub(:safe_message).and_return(message + "\n" + message)
+ decorator.title.should == message
+ end
+
+ it "truncates a message with a newline after 80 characters at 70 characters" do
+ message = (commit.safe_message * 10) + "\n"
+
+ decorator.stub(:safe_message).and_return(message)
+ decorator.title.should == "#{message[0..69]}…"
+ end
+ end
+ end
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 5cb68761..77b49246 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -1,3 +1,19 @@
+# == Schema Information
+#
+# Table name: events
+#
+# id :integer not null, primary key
+# target_type :string(255)
+# target_id :integer
+# title :string(255)
+# data :text
+# project_id :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# action :integer
+# author_id :integer
+#
+
require 'spec_helper'
describe Event do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
new file mode 100644
index 00000000..5ae40658
--- /dev/null
+++ b/spec/models/group_spec.rb
@@ -0,0 +1,24 @@
+# == Schema Information
+#
+# Table name: groups
+#
+# id :integer not null, primary key
+# name :string(255) not null
+# code :string(255) not null
+# owner_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
+require 'spec_helper'
+
+describe Group do
+ let!(:group) { create(:group) }
+
+ it { should have_many :projects }
+ it { should validate_presence_of :name }
+ it { should validate_uniqueness_of(:name) }
+ it { should validate_presence_of :code }
+ it { should validate_uniqueness_of(:code) }
+ it { should validate_presence_of :owner }
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 34192da9..7c98b9ea 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -1,3 +1,21 @@
+# == Schema Information
+#
+# Table name: issues
+#
+# id :integer not null, primary key
+# title :string(255)
+# assignee_id :integer
+# author_id :integer
+# project_id :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# closed :boolean default(FALSE), not null
+# position :integer default(0)
+# branch_name :string(255)
+# description :text
+# milestone_id :integer
+#
+
require 'spec_helper'
describe Issue do
@@ -5,6 +23,11 @@ describe Issue do
it { should belong_to(:milestone) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:author_id) }
+ it { should_not allow_mass_assignment_of(:project_id) }
+ end
+
describe "Validation" do
it { should ensure_length_of(:description).is_within(0..2000) }
it { should ensure_inclusion_of(:closed).in_array([true, false]) }
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 85cd291d..d3231af8 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -1,3 +1,17 @@
+# == Schema Information
+#
+# Table name: keys
+#
+# id :integer not null, primary key
+# user_id :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# key :text
+# title :string(255)
+# identifier :string(255)
+# project_id :integer
+#
+
require 'spec_helper'
describe Key do
@@ -6,6 +20,11 @@ describe Key do
it { should belong_to(:project) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:project_id) }
+ it { should_not allow_mass_assignment_of(:user_id) }
+ end
+
describe "Validation" do
it { should validate_presence_of(:title) }
it { should validate_presence_of(:key) }
@@ -46,4 +65,16 @@ describe Key do
end
end
end
+
+ context "validate it is a fingerprintable key" do
+ let(:user) { Factory.create(:user) }
+
+ it "accepts the fingerprintable key" do
+ build(:key, user: user).should be_valid
+ end
+
+ it "rejects the unfingerprintable key" do
+ build(:key_with_a_space_in_the_middle).should_not be_valid
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 523e823d..be40c561 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1,3 +1,23 @@
+# == Schema Information
+#
+# Table name: merge_requests
+#
+# id :integer not null, primary key
+# target_branch :string(255) not null
+# source_branch :string(255) not null
+# project_id :integer not null
+# author_id :integer
+# assignee_id :integer
+# title :string(255)
+# closed :boolean default(FALSE), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# st_commits :text(4294967295
+# st_diffs :text(4294967295
+# merged :boolean default(FALSE), not null
+# state :integer default(1), not null
+#
+
require 'spec_helper'
describe MergeRequest do
@@ -6,8 +26,71 @@ describe MergeRequest do
it { should validate_presence_of(:source_branch) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:author_id) }
+ it { should_not allow_mass_assignment_of(:project_id) }
+ end
+
describe 'modules' do
it { should include_module(IssueCommonality) }
it { should include_module(Votes) }
end
+
+ describe "#mr_and_commit_notes" do
+ let!(:merge_request) { Factory.create(:merge_request) }
+
+ before do
+ merge_request.stub(:commits) { [merge_request.project.commit] }
+ Factory.create(:note, noteable: merge_request.commits.first)
+ Factory.create(:note, noteable: merge_request)
+ end
+
+ it "should include notes for commits" do
+ merge_request.commits.should_not be_empty
+ merge_request.mr_and_commit_notes.count.should == 2
+ end
+ end
+
+ subject { Factory.create(:merge_request) }
+
+ describe '#is_being_reassigned?' do
+ it 'returns true if the merge_request assignee has changed' do
+ subject.assignee = Factory(:user)
+ subject.is_being_reassigned?.should be_true
+ end
+ it 'returns false if the merge request assignee has not changed' do
+ subject.is_being_reassigned?.should be_false
+ end
+ end
+
+ describe '#is_being_closed?' do
+ it 'returns true if the closed attribute has changed and is now true' do
+ subject.closed = true
+ subject.is_being_closed?.should be_true
+ end
+ it 'returns false if the closed attribute has changed and is now false' do
+ merge_request = Factory.create(:closed_merge_request)
+ merge_request.closed = false
+ merge_request.is_being_closed?.should be_false
+ end
+ it 'returns false if the closed attribute has not changed' do
+ subject.is_being_closed?.should be_false
+ end
+ end
+
+
+ describe '#is_being_reopened?' do
+ it 'returns true if the closed attribute has changed and is now false' do
+ merge_request = Factory.create(:closed_merge_request)
+ merge_request.closed = false
+ merge_request.is_being_reopened?.should be_true
+ end
+ it 'returns false if the closed attribute has changed and is now true' do
+ subject.closed = true
+ subject.is_being_reopened?.should be_false
+ end
+ it 'returns false if the closed attribute has not changed' do
+ subject.is_being_reopened?.should be_false
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index f0f0f883..1aba20c6 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -1,3 +1,17 @@
+# == Schema Information
+#
+# Table name: milestones
+#
+# id :integer not null, primary key
+# title :string(255) not null
+# project_id :integer not null
+# description :text
+# due_date :date
+# closed :boolean default(FALSE), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
require 'spec_helper'
describe Milestone do
@@ -6,9 +20,13 @@ describe Milestone do
it { should have_many(:issues) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:project_id) }
+ end
+
describe "Validation" do
it { should validate_presence_of(:title) }
- it { should validate_presence_of(:project_id) }
+ it { should validate_presence_of(:project) }
it { should ensure_inclusion_of(:closed).in_array([true, false]) }
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 7809953f..514b6202 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1,3 +1,19 @@
+# == Schema Information
+#
+# Table name: notes
+#
+# id :integer not null, primary key
+# note :text
+# noteable_id :string(255)
+# noteable_type :string(255)
+# author_id :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# project_id :integer
+# attachment :string(255)
+# line_code :string(255)
+#
+
require 'spec_helper'
describe Note do
@@ -7,6 +23,11 @@ describe Note do
it { should belong_to(:author).class_name('User') }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:author) }
+ it { should_not allow_mass_assignment_of(:author_id) }
+ end
+
describe "Validation" do
it { should validate_presence_of(:note) }
it { should validate_presence_of(:project) }
@@ -64,9 +85,19 @@ describe Note do
noteable_type: "Commit"
end
+ it "should be accessible through #noteable" do
+ @note.noteable_id.should == commit.id
+ @note.noteable.should be_a(Commit)
+ @note.noteable.should == commit
+ end
+
it "should save a valid note" do
@note.noteable_id.should == commit.id
- @note.target.id.should == commit.id
+ @note.noteable == commit
+ end
+
+ it "should be recognized by #for_commit?" do
+ @note.should be_for_commit
end
end
@@ -80,7 +111,11 @@ describe Note do
it "should save a valid note" do
@note.noteable_id.should == commit.id
- @note.target.id.should == commit.id
+ @note.noteable.id.should == commit.id
+ end
+
+ it "should be recognized by #for_diff_line?" do
+ @note.should be_for_diff_line
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 756f69de..6fe46446 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1,7 +1,29 @@
+# == Schema Information
+#
+# Table name: projects
+#
+# id :integer not null, primary key
+# name :string(255)
+# path :string(255)
+# description :text
+# created_at :datetime not null
+# updated_at :datetime not null
+# private_flag :boolean default(TRUE), not null
+# code :string(255)
+# owner_id :integer
+# default_branch :string(255)
+# issues_enabled :boolean default(TRUE), not null
+# wall_enabled :boolean default(TRUE), not null
+# merge_requests_enabled :boolean default(TRUE), not null
+# wiki_enabled :boolean default(TRUE), not null
+# group_id :integer
+#
+
require 'spec_helper'
describe Project do
describe "Associations" do
+ it { should belong_to(:group) }
it { should belong_to(:owner).class_name('User') }
it { should have_many(:users) }
it { should have_many(:events).dependent(:destroy) }
@@ -17,6 +39,11 @@ describe Project do
it { should have_many(:protected_branches).dependent(:destroy) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:owner_id) }
+ it { should_not allow_mass_assignment_of(:private_flag) }
+ end
+
describe "Validation" do
let!(:project) { create(:project) }
@@ -120,7 +147,7 @@ describe Project do
it "should return path to repo" do
project = Project.new(path: "somewhere")
- project.path_to_repo.should == File.join(Rails.root, "tmp", "repositories", "somewhere")
+ project.path_to_repo.should == Rails.root.join("tmp", "repositories", "somewhere")
end
it "returns the full web URL for this repo" do
@@ -140,30 +167,29 @@ describe Project do
end
end
- describe "last_activity" do
+ describe "last_activity methods" do
let(:project) { Factory :project }
- let(:last_event) { double }
+ let(:last_event) { double(created_at: Time.now) }
- before do
- project.stub_chain(:events, :order).and_return( [ double, double, last_event ] )
+ describe "last_activity" do
+ it "should alias last_activity to last_event"do
+ project.stub(last_event: last_event)
+ project.last_activity.should == last_event
+ end
end
- it { project.last_activity.should == last_event }
+ describe 'last_activity_date' do
+ it 'returns the creation date of the project\'s last event if present' do
+ project.stub(last_event: last_event)
+ project.last_activity_date.should == last_event.created_at
+ end
+
+ it 'returns the project\'s last update date if it has no events' do
+ project.last_activity_date.should == project.updated_at
+ end
+ end
end
- describe 'last_activity_date' do
- let(:project) { Factory :project }
-
- it 'returns the creation date of the project\'s last event if present' do
- last_event = double(created_at: 'now')
- project.stub(:events).and_return( [double, double, last_event] )
- project.last_activity_date.should == last_event.created_at
- end
-
- it 'returns the project\'s last update date if it has no events' do
- project.last_activity_date.should == project.updated_at
- end
- end
describe "fresh commits" do
let(:project) { Factory :project }
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index 9180bc3b..874c4e4d 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -1,3 +1,14 @@
+# == Schema Information
+#
+# Table name: protected_branches
+#
+# id :integer not null, primary key
+# project_id :integer not null
+# name :string(255) not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
require 'spec_helper'
describe ProtectedBranch do
@@ -5,8 +16,12 @@ describe ProtectedBranch do
it { should belong_to(:project) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:project_id) }
+ end
+
describe 'Validation' do
- it { should validate_presence_of(:project_id) }
+ it { should validate_presence_of(:project) }
it { should validate_presence_of(:name) }
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index ffb861c4..ada5fcdb 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -1,3 +1,18 @@
+# == Schema Information
+#
+# Table name: snippets
+#
+# id :integer not null, primary key
+# title :string(255)
+# content :text
+# author_id :integer not null
+# project_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# file_name :string(255)
+# expires_at :datetime
+#
+
require 'spec_helper'
describe Snippet do
@@ -7,9 +22,14 @@ describe Snippet do
it { should have_many(:notes).dependent(:destroy) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:author_id) }
+ it { should_not allow_mass_assignment_of(:project_id) }
+ end
+
describe "Validation" do
- it { should validate_presence_of(:author_id) }
- it { should validate_presence_of(:project_id) }
+ it { should validate_presence_of(:author) }
+ it { should validate_presence_of(:project) }
it { should validate_presence_of(:title) }
it { should ensure_length_of(:title).is_within(0..255) }
diff --git a/spec/models/system_hook_spec.rb b/spec/models/system_hook_spec.rb
index fe2a5836..b5d338a8 100644
--- a/spec/models/system_hook_spec.rb
+++ b/spec/models/system_hook_spec.rb
@@ -1,3 +1,15 @@
+# == Schema Information
+#
+# Table name: web_hooks
+#
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# type :string(255) default("ProjectHook")
+#
+
require "spec_helper"
describe SystemHook do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 08176754..5f41fb05 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1,3 +1,37 @@
+# == Schema Information
+#
+# Table name: users
+#
+# id :integer not null, primary key
+# email :string(255) default(""), not null
+# encrypted_password :string(128) default(""), not null
+# reset_password_token :string(255)
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# sign_in_count :integer default(0)
+# current_sign_in_at :datetime
+# last_sign_in_at :datetime
+# current_sign_in_ip :string(255)
+# last_sign_in_ip :string(255)
+# created_at :datetime not null
+# updated_at :datetime not null
+# name :string(255)
+# admin :boolean default(FALSE), not null
+# projects_limit :integer default(10)
+# skype :string(255) default(""), not null
+# linkedin :string(255) default(""), not null
+# twitter :string(255) default(""), not null
+# authentication_token :string(255)
+# dark_scheme :boolean default(FALSE), not null
+# theme_id :integer default(1), not null
+# bio :string(255)
+# blocked :boolean default(FALSE), not null
+# failed_attempts :integer default(0)
+# locked_at :datetime
+# extern_uid :string(255)
+# provider :string(255)
+#
+
require 'spec_helper'
describe User do
@@ -15,6 +49,11 @@ describe User do
it { should have_many(:assigned_merge_requests).dependent(:destroy) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:projects_limit) }
+ it { should allow_mass_assignment_of(:projects_limit).as(:admin) }
+ end
+
describe 'validations' do
it { should validate_presence_of(:projects_limit) }
it { should validate_numericality_of(:projects_limit) }
diff --git a/spec/models/users_project_spec.rb b/spec/models/users_project_spec.rb
index 33cb358e..5b6516b3 100644
--- a/spec/models/users_project_spec.rb
+++ b/spec/models/users_project_spec.rb
@@ -1,3 +1,15 @@
+# == Schema Information
+#
+# Table name: users_projects
+#
+# id :integer not null, primary key
+# user_id :integer not null
+# project_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# project_access :integer default(0), not null
+#
+
require 'spec_helper'
describe UsersProject do
@@ -6,13 +18,17 @@ describe UsersProject do
it { should belong_to(:user) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:project_id) }
+ end
+
describe "Validation" do
let!(:users_project) { create(:users_project) }
- it { should validate_presence_of(:user_id) }
+ it { should validate_presence_of(:user) }
it { should validate_uniqueness_of(:user_id).scoped_to(:project_id).with_message(/already exists/) }
- it { should validate_presence_of(:project_id) }
+ it { should validate_presence_of(:project) }
end
describe "Delegate methods" do
diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb
index 3cba5b64..d71fec81 100644
--- a/spec/models/web_hook_spec.rb
+++ b/spec/models/web_hook_spec.rb
@@ -1,3 +1,15 @@
+# == Schema Information
+#
+# Table name: web_hooks
+#
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# type :string(255) default("ProjectHook")
+#
+
require 'spec_helper'
describe ProjectHook do
@@ -5,6 +17,10 @@ describe ProjectHook do
it { should belong_to :project }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:project_id) }
+ end
+
describe "Validations" do
it { should validate_presence_of(:url) }
diff --git a/spec/models/wiki_spec.rb b/spec/models/wiki_spec.rb
index de6ce426..96aebd2d 100644
--- a/spec/models/wiki_spec.rb
+++ b/spec/models/wiki_spec.rb
@@ -1,3 +1,17 @@
+# == Schema Information
+#
+# Table name: wikis
+#
+# id :integer not null, primary key
+# title :string(255)
+# content :text
+# project_id :integer
+# created_at :datetime not null
+# updated_at :datetime not null
+# slug :string(255)
+# user_id :integer
+#
+
require 'spec_helper'
describe Wiki do
@@ -7,10 +21,15 @@ describe Wiki do
it { should have_many(:notes).dependent(:destroy) }
end
+ describe "Mass assignment" do
+ it { should_not allow_mass_assignment_of(:project_id) }
+ it { should_not allow_mass_assignment_of(:user_id) }
+ end
+
describe "Validation" do
it { should validate_presence_of(:title) }
it { should ensure_length_of(:title).is_within(1..250) }
it { should validate_presence_of(:content) }
- it { should validate_presence_of(:user_id) }
+ it { should validate_presence_of(:user) }
end
end
diff --git a/spec/observers/merge_request_observer_spec.rb b/spec/observers/merge_request_observer_spec.rb
new file mode 100644
index 00000000..a9ba7927
--- /dev/null
+++ b/spec/observers/merge_request_observer_spec.rb
@@ -0,0 +1,193 @@
+require 'spec_helper'
+
+describe MergeRequestObserver do
+ let(:some_user) { double(:user, id: 1) }
+ let(:assignee) { double(:user, id: 2) }
+ let(:author) { double(:user, id: 3) }
+ let(:mr) { double(:merge_request, id: 42, assignee: assignee, author: author) }
+
+ before(:each) { subject.stub(:current_user).and_return(some_user) }
+
+ subject { MergeRequestObserver.instance }
+
+ describe '#after_create' do
+
+ it 'is called when a merge request is created' do
+ subject.should_receive(:after_create)
+
+ MergeRequest.observers.enable :merge_request_observer do
+ Factory.create(:merge_request, project: Factory.create(:project))
+ end
+ end
+
+ it 'sends an email to the assignee' do
+ Notify.should_receive(:new_merge_request_email).with(mr.id).
+ and_return(double(deliver: true))
+
+ subject.after_create(mr)
+ end
+
+ it 'does not send an email to the assignee if assignee created the merge request' do
+ subject.stub(:current_user).and_return(assignee)
+ Notify.should_not_receive(:new_merge_request_email)
+
+ subject.after_create(mr)
+ end
+ end
+
+ context '#after_update' do
+ before(:each) do
+ mr.stub(:is_being_reassigned?).and_return(false)
+ mr.stub(:is_being_closed?).and_return(false)
+ mr.stub(:is_being_reopened?).and_return(false)
+ end
+
+ it 'is called when a merge request is changed' do
+ changed = Factory.create(:merge_request, project: Factory.create(:project))
+ subject.should_receive(:after_update)
+
+ MergeRequest.observers.enable :merge_request_observer do
+ changed.title = 'I changed'
+ changed.save
+ end
+ end
+
+ context 'a reassigned email' do
+ it 'is sent if the merge request is being reassigned' do
+ mr.should_receive(:is_being_reassigned?).and_return(true)
+ subject.should_receive(:send_reassigned_email).with(mr)
+
+ subject.after_update(mr)
+ end
+
+ it 'is not sent if the merge request is not being reassigned' do
+ mr.should_receive(:is_being_reassigned?).and_return(false)
+ subject.should_not_receive(:send_reassigned_email)
+
+ subject.after_update(mr)
+ end
+ end
+
+ context 'a status "closed"' do
+ it 'note is created if the merge request is being closed' do
+ mr.should_receive(:is_being_closed?).and_return(true)
+ Note.should_receive(:create_status_change_note).with(mr, some_user, 'closed')
+
+ subject.after_update(mr)
+ end
+
+ it 'note is not created if the merge request is not being closed' do
+ mr.should_receive(:is_being_closed?).and_return(false)
+ Note.should_not_receive(:create_status_change_note).with(mr, some_user, 'closed')
+
+ subject.after_update(mr)
+ end
+
+ it 'notification is delivered if the merge request being closed' do
+ mr.stub(:is_being_closed?).and_return(true)
+ Note.should_receive(:create_status_change_note).with(mr, some_user, 'closed')
+
+ subject.after_update(mr)
+ end
+
+ it 'notification is not delivered if the merge request not being closed' do
+ mr.stub(:is_being_closed?).and_return(false)
+ Note.should_not_receive(:create_status_change_note).with(mr, some_user, 'closed')
+
+ subject.after_update(mr)
+ end
+
+ it 'notification is delivered only to author if the merge request is being closed' do
+ mr_without_assignee = double(:merge_request, id: 42, author: author, assignee: nil)
+ mr_without_assignee.stub(:is_being_reassigned?).and_return(false)
+ mr_without_assignee.stub(:is_being_closed?).and_return(true)
+ mr_without_assignee.stub(:is_being_reopened?).and_return(false)
+ Note.should_receive(:create_status_change_note).with(mr_without_assignee, some_user, 'closed')
+
+ subject.after_update(mr_without_assignee)
+ end
+ end
+
+ context 'a status "reopened"' do
+ it 'note is created if the merge request is being reopened' do
+ mr.should_receive(:is_being_reopened?).and_return(true)
+ Note.should_receive(:create_status_change_note).with(mr, some_user, 'reopened')
+
+ subject.after_update(mr)
+ end
+
+ it 'note is not created if the merge request is not being reopened' do
+ mr.should_receive(:is_being_reopened?).and_return(false)
+ Note.should_not_receive(:create_status_change_note).with(mr, some_user, 'reopened')
+
+ subject.after_update(mr)
+ end
+
+ it 'notification is delivered if the merge request being reopened' do
+ mr.stub(:is_being_reopened?).and_return(true)
+ Note.should_receive(:create_status_change_note).with(mr, some_user, 'reopened')
+
+ subject.after_update(mr)
+ end
+
+ it 'notification is not delivered if the merge request is not being reopened' do
+ mr.stub(:is_being_reopened?).and_return(false)
+ Note.should_not_receive(:create_status_change_note).with(mr, some_user, 'reopened')
+
+ subject.after_update(mr)
+ end
+
+ it 'notification is delivered only to author if the merge request is being reopened' do
+ mr_without_assignee = double(:merge_request, id: 42, author: author, assignee: nil)
+ mr_without_assignee.stub(:is_being_reassigned?).and_return(false)
+ mr_without_assignee.stub(:is_being_closed?).and_return(false)
+ mr_without_assignee.stub(:is_being_reopened?).and_return(true)
+ Note.should_receive(:create_status_change_note).with(mr_without_assignee, some_user, 'reopened')
+
+ subject.after_update(mr_without_assignee)
+ end
+ end
+ end
+
+ describe '#send_reassigned_email' do
+ let(:previous_assignee) { double(:user, id: 3) }
+
+ before(:each) do
+ mr.stub(:assignee_id).and_return(assignee.id)
+ mr.stub(:assignee_id_was).and_return(previous_assignee.id)
+ end
+
+ def it_sends_a_reassigned_email_to(recipient)
+ Notify.should_receive(:reassigned_merge_request_email).with(recipient, mr.id, previous_assignee.id).
+ and_return(double(deliver: true))
+ end
+
+ def it_does_not_send_a_reassigned_email_to(recipient)
+ Notify.should_not_receive(:reassigned_merge_request_email).with(recipient, mr.id, previous_assignee.id)
+ end
+
+ it 'sends a reassigned email to the previous and current assignees' do
+ it_sends_a_reassigned_email_to assignee.id
+ it_sends_a_reassigned_email_to previous_assignee.id
+
+ subject.send(:send_reassigned_email, mr)
+ end
+
+ context 'does not send an email to the user who made the reassignment' do
+ it 'if the user is the assignee' do
+ subject.stub(:current_user).and_return(assignee)
+ it_sends_a_reassigned_email_to previous_assignee.id
+ it_does_not_send_a_reassigned_email_to assignee.id
+
+ subject.send(:send_reassigned_email, mr)
+ end
+ it 'if the user is the previous assignee' do
+ subject.stub(:current_user).and_return(previous_assignee)
+ it_sends_a_reassigned_email_to assignee.id
+ it_does_not_send_a_reassigned_email_to previous_assignee.id
+
+ subject.send(:send_reassigned_email, mr)
+ end
+ end
+ end
+end
diff --git a/spec/observers/note_observer_spec.rb b/spec/observers/note_observer_spec.rb
new file mode 100644
index 00000000..203a58a4
--- /dev/null
+++ b/spec/observers/note_observer_spec.rb
@@ -0,0 +1,128 @@
+require 'spec_helper'
+
+describe NoteObserver do
+ subject { NoteObserver.instance }
+
+ let(:team_without_author) { (1..2).map { |n| double :user, id: n } }
+ let(:delivery_success) { double deliver: true }
+
+ describe '#after_create' do
+ let(:note) { double :note }
+
+ it 'is called after a note is created' do
+ subject.should_receive :after_create
+
+ Note.observers.enable :note_observer do
+ Factory.create(:note)
+ end
+ end
+
+ it 'sends out notifications' do
+ subject.should_receive(:send_notify_mails).with(note)
+
+ subject.after_create(note)
+ end
+ end
+
+ describe "#send_notify_mails" do
+ let(:note) { double :note, notify: false, notify_author: false }
+
+ it 'notifies team of new note when flagged to notify' do
+ note.stub(:notify).and_return(true)
+ subject.should_receive(:notify_team).with(note)
+
+ subject.after_create(note)
+ end
+
+ it 'does not notify team of new note when not flagged to notify' do
+ subject.should_not_receive(:notify_team).with(note)
+
+ subject.after_create(note)
+ end
+
+ it 'notifies the author of a commit when flagged to notify the author' do
+ note.stub(:notify_author).and_return(true)
+ note.stub(:id).and_return(42)
+ author = double :user, id: 1
+ note.stub(:commit_author).and_return(author)
+ Notify.should_receive(:note_commit_email).and_return(delivery_success)
+
+ subject.after_create(note)
+ end
+
+ it 'does not notify the author of a commit when not flagged to notify the author' do
+ Notify.should_not_receive(:note_commit_email)
+
+ subject.after_create(note)
+ end
+
+ it 'does nothing if no notify flags are set' do
+ subject.after_create(note).should be_nil
+ end
+ end
+
+ describe '#notify_team' do
+ let(:note) { double :note, id: 1 }
+
+ before :each do
+ subject.stub(:team_without_note_author).with(note).and_return(team_without_author)
+ end
+
+ context 'notifies team of a new note on' do
+ it 'a commit' do
+ note.stub(:noteable_type).and_return('Commit')
+ Notify.should_receive(:note_commit_email).twice.and_return(delivery_success)
+
+ subject.send(:notify_team, note)
+ end
+
+ it 'an issue' do
+ note.stub(:noteable_type).and_return('Issue')
+ Notify.should_receive(:note_issue_email).twice.and_return(delivery_success)
+
+ subject.send(:notify_team, note)
+ end
+
+ it 'a wiki page' do
+ note.stub(:noteable_type).and_return('Wiki')
+ Notify.should_receive(:note_wiki_email).twice.and_return(delivery_success)
+
+ subject.send(:notify_team, note)
+ end
+
+ it 'a merge request' do
+ note.stub(:noteable_type).and_return('MergeRequest')
+ Notify.should_receive(:note_merge_request_email).twice.and_return(delivery_success)
+
+ subject.send(:notify_team, note)
+ end
+
+ it 'a wall' do
+ # Note: wall posts have #noteable_type of nil
+ note.stub(:noteable_type).and_return(nil)
+ Notify.should_receive(:note_wall_email).twice.and_return(delivery_success)
+
+ subject.send(:notify_team, note)
+ end
+ end
+
+ it 'does nothing for a new note on a snippet' do
+ note.stub(:noteable_type).and_return('Snippet')
+
+ subject.send(:notify_team, note).should be_nil
+ end
+ end
+
+
+ describe '#team_without_note_author' do
+ let(:author) { double :user, id: 4 }
+
+ let(:users) { team_without_author + [author] }
+ let(:project) { double :project, users: users }
+ let(:note) { double :note, project: project, author: author }
+
+ it 'returns the projects user without the note author included' do
+ subject.send(:team_without_note_author, note).should == team_without_author
+ end
+ end
+end
diff --git a/spec/observers/users_project_observer_spec.rb b/spec/observers/users_project_observer_spec.rb
index 07d71da8..a8e0834b 100644
--- a/spec/observers/users_project_observer_spec.rb
+++ b/spec/observers/users_project_observer_spec.rb
@@ -1,23 +1,27 @@
require 'spec_helper'
describe UsersProjectObserver do
- let(:users_project) { stub.as_null_object }
+ let(:user) { Factory.create :user }
+ let(:project) { Factory.create(:project,
+ code: "Fuu",
+ path: "Fuu" ) }
+ let(:users_project) { Factory.create(:users_project,
+ project: project,
+ user: user )}
subject { UsersProjectObserver.instance }
- describe "#after_create" do
+ describe "#after_commit" do
it "should called when UsersProject created" do
- subject.should_receive(:after_create)
-
+ subject.should_receive(:after_commit).once
UsersProject.observers.enable :users_project_observer do
create(:users_project)
end
end
it "should send email to user" do
+ Notify.should_receive(:project_access_granted_email).with(users_project.id).and_return(double(deliver: true))
+ subject.after_commit(users_project)
Event.stub(:create => true)
- Notify.should_receive(:project_access_granted_email).and_return(stub(deliver: true))
-
- subject.after_create(users_project)
end
it "should create new event" do
@@ -33,17 +37,21 @@ describe UsersProjectObserver do
describe "#after_update" do
it "should called when UsersProject updated" do
- subject.should_receive(:after_update)
-
+ subject.should_receive(:after_commit).once
UsersProject.observers.enable :users_project_observer do
- create(:users_project).update_attribute(:project_access, 40)
+ create(:users_project).update_attribute(:project_access, UsersProject::MASTER)
end
end
it "should send email to user" do
Notify.should_receive(:project_access_granted_email).with(users_project.id).and_return(double(deliver: true))
-
- subject.after_update(users_project)
+ subject.after_commit(users_project)
+ end
+ it "should not called after UsersProject destroyed" do
+ subject.should_not_receive(:after_commit)
+ UsersProject.observers.enable :users_project_observer do
+ users_project.destroy
+ end
end
end
diff --git a/spec/requests/admin/admin_projects_spec.rb b/spec/requests/admin/admin_projects_spec.rb
index 2edfb592..61e66eec 100644
--- a/spec/requests/admin/admin_projects_spec.rb
+++ b/spec/requests/admin/admin_projects_spec.rb
@@ -87,7 +87,7 @@ describe "Admin::Projects" do
visit new_admin_project_path
fill_in 'project_name', with: 'NewProject'
fill_in 'project_code', with: 'NPR'
- fill_in 'project_path', with: 'newproject'
+ fill_in 'project_path', with: 'gitlabhq_1'
expect { click_button "Create project" }.to change { Project.count }.by(1)
@project = Project.last
end
@@ -102,13 +102,13 @@ describe "Admin::Projects" do
end
end
- describe "Add new team member" do
- before do
+ describe "Add new team member" do
+ before do
@new_user = Factory :user
visit admin_project_path(@project)
end
- it "should create new user" do
+ it "should create new user" do
select @new_user.name, from: "user_ids"
expect { click_button "Add" }.to change { UsersProject.count }.by(1)
page.should have_content @new_user.name
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 498bbad6..51526f89 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -172,7 +172,15 @@ describe Gitlab::API do
end
end
- describe "POST /projects/:id/users" do
+ describe "GET /projects/:id/hooks/:hook_id" do
+ it "should return a project hook" do
+ get api("/projects/#{project.code}/hooks/#{hook.id}", user)
+ response.status.should == 200
+ json_response['url'].should == hook.url
+ end
+ end
+
+ describe "POST /projects/:id/hooks" do
it "should add hook to project" do
expect {
post api("/projects/#{project.code}/hooks", user),
@@ -180,6 +188,16 @@ describe Gitlab::API do
}.to change {project.hooks.count}.by(1)
end
end
+
+ describe "PUT /projects/:id/hooks/:hook_id" do
+ it "should update an existing project hook" do
+ put api("/projects/#{project.code}/hooks/#{hook.id}", user),
+ url: 'http://example.com'
+ response.status.should == 200
+ json_response['url'].should == 'http://example.com'
+ end
+ end
+
describe "DELETE /projects/:id/hooks" do
it "should delete hook from project" do
@@ -220,6 +238,15 @@ describe Gitlab::API do
end
end
+ describe "GET /projects/:id/snippets" do
+ it "should return a project snippet" do
+ get api("/projects/#{project.code}/snippets", user)
+ response.status.should == 200
+ json_response.should be_an Array
+ json_response.first['title'].should == snippet.title
+ end
+ end
+
describe "GET /projects/:id/snippets/:snippet_id" do
it "should return a project snippet" do
get api("/projects/#{project.code}/snippets/#{snippet.id}", user)
@@ -237,7 +264,7 @@ describe Gitlab::API do
end
end
- describe "PUT /projects/:id/snippets" do
+ describe "PUT /projects/:id/snippets/:shippet_id" do
it "should update an existing project snippet" do
put api("/projects/#{project.code}/snippets/#{snippet.id}", user),
code: 'updated code'
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 243f70f5..e3049e09 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::API do
include ApiHelpers
let(:user) { Factory :user }
+ let(:admin) {Factory :admin}
let(:key) { Factory :key, user: user }
describe "GET /users" do
@@ -32,6 +33,26 @@ describe Gitlab::API do
end
end
+ describe "POST /users" do
+ before{ admin }
+
+ it "should not create invalid user" do
+ post api("/users", admin), { email: "invalid email" }
+ response.status.should == 404
+ end
+
+ it "should create user" do
+ expect{
+ post api("/users", admin), Factory.attributes(:user)
+ }.to change{User.count}.by(1)
+ end
+
+ it "shouldn't available for non admin users" do
+ post api("/users", user), Factory.attributes(:user)
+ response.status.should == 403
+ end
+ end
+
describe "GET /user" do
it "should return current user" do
get api("/user", user)
diff --git a/spec/requests/atom/dashboard_spec.rb b/spec/requests/atom/dashboard_spec.rb
index 9459dd01..c160d24a 100644
--- a/spec/requests/atom/dashboard_spec.rb
+++ b/spec/requests/atom/dashboard_spec.rb
@@ -10,12 +10,5 @@ describe "Dashboard Feed" do
page.body.should have_selector("feed title")
end
end
-
- context "projects page via private token" do
- it "should redirect to login page" do
- visit dashboard_path(private_token: user.private_token)
- current_path.should == new_user_session_path
- end
- end
end
end
diff --git a/spec/requests/gitlab_flavored_markdown_spec.rb b/spec/requests/gitlab_flavored_markdown_spec.rb
index 68d354b7..106f6451 100644
--- a/spec/requests/gitlab_flavored_markdown_spec.rb
+++ b/spec/requests/gitlab_flavored_markdown_spec.rb
@@ -40,28 +40,27 @@ describe "Gitlab Flavored Markdown" do
project.add_access(@user, :read, :write)
end
-
describe "for commits" do
it "should render title in commits#index" do
- visit project_commits_path(project, ref: @branch_name)
+ visit project_commits_path(project, @branch_name, limit: 1)
page.should have_link("##{issue.id}")
end
it "should render title in commits#show" do
- visit project_commit_path(project, id: commit.id)
+ visit project_commit_path(project, commit)
page.should have_link("##{issue.id}")
end
it "should render description in commits#show" do
- visit project_commit_path(project, id: commit.id)
+ visit project_commit_path(project, commit)
page.should have_link("@#{fred.name}")
end
it "should render title in refs#tree", js: true do
- visit tree_project_ref_path(project, id: @branch_name)
+ visit project_tree_path(project, @branch_name)
within(".tree_commit") do
page.should have_link("##{issue.id}")
@@ -69,7 +68,7 @@ describe "Gitlab Flavored Markdown" do
end
it "should render title in refs#blame" do
- visit blame_file_project_ref_path(project, id: @branch_name, path: @test_file)
+ visit project_blame_path(project, File.join(@branch_name, @test_file))
within(".blame_commit") do
page.should have_link("##{issue.id}")
@@ -89,7 +88,6 @@ describe "Gitlab Flavored Markdown" do
end
end
-
describe "for issues" do
before do
@other_issue = Factory :issue,
@@ -175,7 +173,7 @@ describe "Gitlab Flavored Markdown" do
describe "for notes" do
it "should render in commits#show", js: true do
- visit project_commit_path(project, id: commit.id)
+ visit project_commit_path(project, commit)
fill_in "note_note", with: "see ##{issue.id}"
click_button "Add Comment"
diff --git a/spec/requests/hooks_spec.rb b/spec/requests/hooks_spec.rb
deleted file mode 100644
index 7cfe7cfe..00000000
--- a/spec/requests/hooks_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require 'spec_helper'
-
-describe "Hooks" do
- before do
- login_as :user
- @project = Factory :project
- @project.add_access(@user, :read, :admin)
- end
-
- describe "GET index" do
- it "should be available" do
- @hook = Factory :project_hook, project: @project
- visit project_hooks_path(@project)
- page.should have_content "Hooks"
- page.should have_content @hook.url
- end
- end
-
- describe "New Hook" do
- before do
- @url = Faker::Internet.uri("http")
- visit project_hooks_path(@project)
- fill_in "hook_url", with: @url
- expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1)
- end
-
- it "should open new team member popup" do
- page.current_path.should == project_hooks_path(@project)
- page.should have_content(@url)
- end
- end
-
- describe "Test" do
- before do
- @hook = Factory :project_hook, project: @project
- stub_request(:post, @hook.url)
- visit project_hooks_path(@project)
- click_link "Test Hook"
- end
-
- it { page.current_path.should == project_hooks_path(@project) }
- end
-end
diff --git a/spec/requests/projects_deploy_keys_spec.rb b/spec/requests/projects_deploy_keys_spec.rb
index 894aa6d3..df1be79d 100644
--- a/spec/requests/projects_deploy_keys_spec.rb
+++ b/spec/requests/projects_deploy_keys_spec.rb
@@ -42,7 +42,7 @@ describe "Projects", "DeployKeys" do
describe "fill in" do
before do
fill_in "key_title", with: "laptop"
- fill_in "key_key", with: "ssh-rsa publickey234="
+ fill_in "key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
end
it { expect { click_button "Save" }.to change {Key.count}.by(1) }
diff --git a/spec/requests/security/project_access_spec.rb b/spec/requests/security/project_access_spec.rb
index af0d5fcd..060a276b 100644
--- a/spec/requests/security/project_access_spec.rb
+++ b/spec/requests/security/project_access_spec.rb
@@ -14,204 +14,228 @@ describe "Application access" do
end
describe "Project" do
+ let(:project) { create(:project) }
+
+ let(:master) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:reporter) { create(:user) }
+
before do
- @project = Factory :project
- @u1 = Factory :user
- @u2 = Factory :user
- @u3 = Factory :user
# full access
- @project.users_projects.create(user: @u1, project_access: UsersProject::MASTER)
+ project.users_projects.create(user: master, project_access: UsersProject::MASTER)
+
# readonly
- @project.users_projects.create(user: @u3, project_access: UsersProject::REPORTER)
+ project.users_projects.create(user: reporter, project_access: UsersProject::REPORTER)
end
describe "GET /project_code" do
- subject { project_path(@project) }
+ subject { project_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
- describe "GET /project_code/master/tree" do
- subject { tree_project_ref_path(@project, @project.root_ref) }
+ describe "GET /project_code/tree/master" do
+ subject { project_tree_path(project, project.root_ref) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
- describe "GET /project_code/commits" do
- subject { project_commits_path(@project) }
+ describe "GET /project_code/commits/master" do
+ subject { project_commits_path(project, project.root_ref, limit: 1) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
- describe "GET /project_code/commit" do
- subject { project_commit_path(@project, @project.commit.id) }
+ describe "GET /project_code/commit/:sha" do
+ subject { project_commit_path(project, project.commit) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
+ it { should be_denied_for :user }
+ it { should be_denied_for :visitor }
+ end
+
+ describe "GET /project_code/compare" do
+ subject { project_compare_index_path(project) }
+
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
+ it { should be_denied_for :admin }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/team" do
- subject { project_team_index_path(@project) }
+ subject { project_team_index_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/wall" do
- subject { wall_project_path(@project) }
+ subject { wall_project_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/blob" do
before do
- commit = @project.commit
+ commit = project.commit
path = commit.tree.contents.select { |i| i.is_a?(Grit::Blob)}.first.name
- @blob_path = blob_project_ref_path(@project, commit.id, path: path)
+ @blob_path = project_blob_path(project, File.join(commit.id, path))
end
- it { @blob_path.should be_allowed_for @u1 }
- it { @blob_path.should be_allowed_for @u3 }
+ it { @blob_path.should be_allowed_for master }
+ it { @blob_path.should be_allowed_for reporter }
it { @blob_path.should be_denied_for :admin }
- it { @blob_path.should be_denied_for @u2 }
+ it { @blob_path.should be_denied_for guest }
it { @blob_path.should be_denied_for :user }
it { @blob_path.should be_denied_for :visitor }
end
describe "GET /project_code/edit" do
- subject { edit_project_path(@project) }
+ subject { edit_project_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_denied_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_denied_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/deploy_keys" do
- subject { project_deploy_keys_path(@project) }
+ subject { project_deploy_keys_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_denied_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_denied_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/issues" do
- subject { project_issues_path(@project) }
+ subject { project_issues_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/snippets" do
- subject { project_snippets_path(@project) }
+ subject { project_snippets_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/merge_requests" do
- subject { project_merge_requests_path(@project) }
+ subject { project_merge_requests_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/repository" do
- subject { project_repository_path(@project) }
+ subject { project_repository_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/repository/branches" do
- subject { branches_project_repository_path(@project) }
+ subject { branches_project_repository_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ before do
+ # Speed increase
+ Project.any_instance.stub(:branches).and_return([])
+ end
+
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/repository/tags" do
- subject { tags_project_repository_path(@project) }
+ subject { tags_project_repository_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ before do
+ # Speed increase
+ Project.any_instance.stub(:tags).and_return([])
+ end
+
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/hooks" do
- subject { project_hooks_path(@project) }
+ subject { project_hooks_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
describe "GET /project_code/files" do
- subject { files_project_path(@project) }
+ subject { files_project_path(project) }
- it { should be_allowed_for @u1 }
- it { should be_allowed_for @u3 }
+ it { should be_allowed_for master }
+ it { should be_allowed_for reporter }
it { should be_denied_for :admin }
- it { should be_denied_for @u2 }
+ it { should be_denied_for guest }
it { should be_denied_for :user }
it { should be_denied_for :visitor }
end
diff --git a/spec/roles/issue_commonality_spec.rb b/spec/roles/issue_commonality_spec.rb
index 77b98b46..fc4114e3 100644
--- a/spec/roles/issue_commonality_spec.rb
+++ b/spec/roles/issue_commonality_spec.rb
@@ -11,8 +11,8 @@ describe Issue, "IssueCommonality" do
end
describe "Validation" do
- it { should validate_presence_of(:project_id) }
- it { should validate_presence_of(:author_id) }
+ it { should validate_presence_of(:project) }
+ it { should validate_presence_of(:author) }
it { should validate_presence_of(:title) }
it { should ensure_length_of(:title).is_at_least(0).is_at_most(255) }
end
diff --git a/spec/roles/repository_spec.rb b/spec/roles/repository_spec.rb
index 0fda57a3..3507585a 100644
--- a/spec/roles/repository_spec.rb
+++ b/spec/roles/repository_spec.rb
@@ -21,27 +21,27 @@ describe Project, "Repository" do
end
describe "#discover_default_branch" do
- let(:master) { double(name: 'master') }
- let(:stable) { double(name: 'stable') }
+ let(:master) { 'master' }
+ let(:stable) { 'stable' }
it "returns 'master' when master exists" do
- project.should_receive(:heads).and_return([stable, master])
+ project.should_receive(:branch_names).at_least(:once).and_return([stable, master])
project.discover_default_branch.should == 'master'
end
it "returns non-master when master exists but default branch is set to something else" do
project.default_branch = 'stable'
- project.should_receive(:heads).and_return([stable, master])
+ project.should_receive(:branch_names).at_least(:once).and_return([stable, master])
project.discover_default_branch.should == 'stable'
end
it "returns a non-master branch when only one exists" do
- project.should_receive(:heads).and_return([stable])
+ project.should_receive(:branch_names).at_least(:once).and_return([stable])
project.discover_default_branch.should == 'stable'
end
it "returns nil when no branch exists" do
- project.should_receive(:heads).and_return([])
+ project.should_receive(:branch_names).at_least(:once).and_return([])
project.discover_default_branch.should be_nil
end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index b3f9db01..dc687d2a 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -192,34 +192,17 @@ describe ProtectedBranchesController, "routing" do
end
# switch_project_refs GET /:project_id/switch(.:format) refs#switch
-# tree_project_ref GET /:project_id/:id/tree(.:format) refs#tree
# logs_tree_project_ref GET /:project_id/:id/logs_tree(.:format) refs#logs_tree
-# blob_project_ref GET /:project_id/:id/blob(.:format) refs#blob
-# tree_file_project_ref GET /:project_id/:id/tree/:path(.:format) refs#tree
# logs_file_project_ref GET /:project_id/:id/logs_tree/:path(.:format) refs#logs_tree
-# blame_file_project_ref GET /:project_id/:id/blame/:path(.:format) refs#blame
describe RefsController, "routing" do
it "to #switch" do
get("/gitlabhq/switch").should route_to('refs#switch', project_id: 'gitlabhq')
end
- it "to #tree" do
- get("/gitlabhq/stable/tree").should route_to('refs#tree', project_id: 'gitlabhq', id: 'stable')
- get("/gitlabhq/stable/tree/foo/bar/baz").should route_to('refs#tree', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz')
- end
-
it "to #logs_tree" do
get("/gitlabhq/stable/logs_tree").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'stable')
get("/gitlabhq/stable/logs_tree/foo/bar/baz").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz')
end
-
- it "to #blob" do
- get("/gitlabhq/stable/blob").should route_to('refs#blob', project_id: 'gitlabhq', id: 'stable')
- end
-
- it "to #blame" do
- get("/gitlabhq/stable/blame/foo/bar/baz").should route_to('refs#blame', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz')
- end
end
# diffs_project_merge_request GET /:project_id/merge_requests/:id/diffs(.:format) merge_requests#diffs
@@ -298,25 +281,22 @@ describe HooksController, "routing" do
end
end
-# compare_project_commits GET /:project_id/commits/compare(.:format) commits#compare
+# project_commit GET /:project_id/commit/:id(.:format) commit#show {:id=>/[[:alnum:]]{6,40}/, :project_id=>/[^\/]+/}
+describe CommitController, "routing" do
+ it "to #show" do
+ get("/gitlabhq/commit/4246fb").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb')
+ get("/gitlabhq/commit/4246fb.patch").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb', format: 'patch')
+ get("/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5')
+ end
+end
+
# patch_project_commit GET /:project_id/commits/:id/patch(.:format) commits#patch
# project_commits GET /:project_id/commits(.:format) commits#index
# POST /:project_id/commits(.:format) commits#create
-# new_project_commit GET /:project_id/commits/new(.:format) commits#new
-# edit_project_commit GET /:project_id/commits/:id/edit(.:format) commits#edit
# project_commit GET /:project_id/commits/:id(.:format) commits#show
-# PUT /:project_id/commits/:id(.:format) commits#update
-# DELETE /:project_id/commits/:id(.:format) commits#destroy
describe CommitsController, "routing" do
- it "to #compare" do
- get("/gitlabhq/commits/compare").should route_to('commits#compare', project_id: 'gitlabhq')
- end
-
- it "to #patch" do
- get("/gitlabhq/commits/1/patch").should route_to('commits#patch', project_id: 'gitlabhq', id: '1')
- end
-
it_behaves_like "RESTful project resources" do
+ let(:actions) { [:show] }
let(:controller) { 'commits' }
end
end
@@ -396,3 +376,42 @@ describe NotesController, "routing" do
let(:controller) { 'notes' }
end
end
+
+# project_blame GET /:project_id/blame/:id(.:format) blame#show {:id=>/.+/, :project_id=>/[^\/]+/}
+describe BlameController, "routing" do
+ it "to #show" do
+ get("/gitlabhq/blame/master/app/models/project.rb").should route_to('blame#show', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
+ end
+end
+
+# project_blob GET /:project_id/blob/:id(.:format) blob#show {:id=>/.+/, :project_id=>/[^\/]+/}
+describe BlobController, "routing" do
+ it "to #show" do
+ get("/gitlabhq/blob/master/app/models/project.rb").should route_to('blob#show', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
+ end
+end
+
+# project_tree GET /:project_id/tree/:id(.:format) tree#show {:id=>/.+/, :project_id=>/[^\/]+/}
+describe TreeController, "routing" do
+ it "to #show" do
+ get("/gitlabhq/tree/master/app/models/project.rb").should route_to('tree#show', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
+ end
+end
+
+# project_compare_index GET /:project_id/compare(.:format) compare#index {:id=>/[^\/]+/, :project_id=>/[^\/]+/}
+# POST /:project_id/compare(.:format) compare#create {:id=>/[^\/]+/, :project_id=>/[^\/]+/}
+# project_compare /:project_id/compare/:from...:to(.:format) compare#show {:from=>/.+/, :to=>/.+/, :id=>/[^\/]+/, :project_id=>/[^\/]+/}
+describe CompareController, "routing" do
+ it "to #index" do
+ get("/gitlabhq/compare").should route_to('compare#index', project_id: 'gitlabhq')
+ end
+
+ it "to #compare" do
+ post("/gitlabhq/compare").should route_to('compare#create', project_id: 'gitlabhq')
+ end
+
+ it "to #show" do
+ get("/gitlabhq/compare/master...stable").should route_to('compare#show', project_id: 'gitlabhq', from: 'master', to: 'stable')
+ get("/gitlabhq/compare/issue/1234...stable").should route_to('compare#show', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable')
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index d381b3f1..4700c3fe 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -28,6 +28,7 @@ RSpec.configure do |config|
config.include LoginHelpers, type: :request
config.include GitoliteStub
config.include FactoryGirl::Syntax::Methods
+ config.include Devise::TestHelpers, type: :controller
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
diff --git a/spec/support/stubbed_repository.rb b/spec/support/stubbed_repository.rb
index 90491e43..5bf3ea46 100644
--- a/spec/support/stubbed_repository.rb
+++ b/spec/support/stubbed_repository.rb
@@ -5,11 +5,11 @@ module StubbedRepository
if new_record? || path == 'newproject'
# There are a couple Project specs and features that expect the Project's
# path to be in the returned path, so let's patronize them.
- File.join(Rails.root, 'tmp', 'repositories', path)
+ Rails.root.join('tmp', 'repositories', path)
else
# For everything else, just give it the path to one of our real seeded
# repos.
- File.join(Rails.root, 'tmp', 'repositories', 'gitlabhq')
+ Rails.root.join('tmp', 'repositories', 'gitlabhq')
end
end
diff --git a/vendor/assets/javascripts/ace-src-noconflict/ace.js b/vendor/assets/javascripts/ace-src-noconflict/ace.js
new file mode 100644
index 00000000..11b4bbb8
--- /dev/null
+++ b/vendor/assets/javascripts/ace-src-noconflict/ace.js
@@ -0,0 +1,15881 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Distributed under the BSD license:
+ *
+ * Copyright (c) 2010, Ajax.org B.V.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of Ajax.org B.V. nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Define a module along with a payload
+ * @param module a name for the payload
+ * @param payload a function to call with (require, exports, module) params
+ */
+
+(function() {
+
+var ACE_NAMESPACE = "ace";
+
+var global = (function() {
+ return this;
+})();
+
+// take care of the case when requirejs is used and we just need to patch it a little bit
+if (!ACE_NAMESPACE && typeof requirejs !== "undefined") {
+
+ var define = global.define;
+ global.define = function(id, deps, callback) {
+ if (typeof callback !== "function")
+ return define.apply(this, arguments);
+
+ return ace.define(id, deps, function(require, exports, module) {
+ if (deps[2] == "module")
+ module.packaged = true;
+ return callback.apply(this, arguments);
+ });
+ };
+ global.define.packaged = true;
+
+ return;
+}
+
+
+var _define = function(module, deps, payload) {
+ if (typeof module !== 'string') {
+ if (_define.original)
+ _define.original.apply(window, arguments);
+ else {
+ console.error('dropping module because define wasn\'t a string.');
+ console.trace();
+ }
+ return;
+ }
+
+ if (arguments.length == 2)
+ payload = deps;
+
+ if (!_define.modules)
+ _define.modules = {};
+
+ _define.modules[module] = payload;
+};
+var _require = function(parentId, module, callback) {
+ if (Object.prototype.toString.call(module) === "[object Array]") {
+ var params = [];
+ for (var i = 0, l = module.length; i < l; ++i) {
+ var dep = lookup(parentId, module[i]);
+ if (!dep && _require.original)
+ return _require.original.apply(window, arguments);
+ params.push(dep);
+ }
+ if (callback) {
+ callback.apply(null, params);
+ }
+ }
+ else if (typeof module === 'string') {
+ var payload = lookup(parentId, module);
+ if (!payload && _require.original)
+ return _require.original.apply(window, arguments);
+
+ if (callback) {
+ callback();
+ }
+
+ return payload;
+ }
+ else {
+ if (_require.original)
+ return _require.original.apply(window, arguments);
+ }
+};
+
+var normalizeModule = function(parentId, moduleName) {
+ // normalize plugin requires
+ if (moduleName.indexOf("!") !== -1) {
+ var chunks = moduleName.split("!");
+ return normalizeModule(parentId, chunks[0]) + "!" + normalizeModule(parentId, chunks[1]);
+ }
+ // normalize relative requires
+ if (moduleName.charAt(0) == ".") {
+ var base = parentId.split("/").slice(0, -1).join("/");
+ moduleName = base + "/" + moduleName;
+
+ while(moduleName.indexOf(".") !== -1 && previous != moduleName) {
+ var previous = moduleName;
+ moduleName = moduleName.replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, "");
+ }
+ }
+
+ return moduleName;
+};
+var lookup = function(parentId, moduleName) {
+
+ moduleName = normalizeModule(parentId, moduleName);
+
+ var module = _define.modules[moduleName];
+ if (!module) {
+ return null;
+ }
+
+ if (typeof module === 'function') {
+ var exports = {};
+ var mod = {
+ id: moduleName,
+ uri: '',
+ exports: exports,
+ packaged: true
+ };
+
+ var req = function(module, callback) {
+ return _require(moduleName, module, callback);
+ };
+
+ var returnValue = module(req, exports, mod);
+ exports = returnValue || mod.exports;
+
+ // cache the resulting module object for next time
+ _define.modules[moduleName] = exports;
+ return exports;
+ }
+
+ return module;
+};
+
+function exportAce(ns) {
+ var require = function(module, callback) {
+ return _require("", module, callback);
+ };
+
+ var root = global;
+ if (ns) {
+ if (!global[ns])
+ global[ns] = {};
+ root = global[ns];
+ }
+
+ if (!root.define || !root.define.packaged) {
+ _define.original = root.define;
+ root.define = _define;
+ root.define.packaged = true;
+ }
+
+ if (!root.require || !root.require.packaged) {
+ _require.original = root.require;
+ root.require = require;
+ root.require.packaged = true;
+ }
+}
+
+exportAce(ACE_NAMESPACE);
+
+})();
+
+/**
+ * class Ace
+ *
+ * The main class required to set up an Ace instance in the browser.
+ *
+ *
+ **/
+
+ace.define('ace/ace', ['require', 'exports', 'module' , 'ace/lib/fixoldbrowsers', 'ace/lib/dom', 'ace/lib/event', 'ace/editor', 'ace/edit_session', 'ace/undomanager', 'ace/virtual_renderer', 'ace/multi_select', 'ace/worker/worker_client', 'ace/keyboard/hash_handler', 'ace/keyboard/state_handler', 'ace/placeholder', 'ace/config', 'ace/theme/textmate'], function(require, exports, module) {
+
+
+require("./lib/fixoldbrowsers");
+
+var Dom = require("./lib/dom");
+var Event = require("./lib/event");
+
+var Editor = require("./editor").Editor;
+var EditSession = require("./edit_session").EditSession;
+var UndoManager = require("./undomanager").UndoManager;
+var Renderer = require("./virtual_renderer").VirtualRenderer;
+var MultiSelect = require("./multi_select").MultiSelect;
+
+// The following require()s are for inclusion in the built ace file
+require("./worker/worker_client");
+require("./keyboard/hash_handler");
+require("./keyboard/state_handler");
+require("./placeholder");
+exports.config = require("./config");
+exports.edit = function(el) {
+ if (typeof(el) == "string") {
+ var _id = el;
+ if (!(el = document.getElementById(el))) {
+ console.log("can't match div #" + _id);
+ }
+ }
+
+ if (el.env && el.env.editor instanceof Editor)
+ return el.env.editor;
+
+ var doc = new EditSession(Dom.getInnerText(el));
+ doc.setUndoManager(new UndoManager());
+ el.innerHTML = '';
+
+ var editor = new Editor(new Renderer(el, require("./theme/textmate")));
+ new MultiSelect(editor);
+ editor.setSession(doc);
+
+ var env = {};
+ env.document = doc;
+ env.editor = editor;
+ editor.resize();
+ Event.addListener(window, "resize", function() {
+ editor.resize();
+ });
+ el.env = env;
+ // Store env on editor such that it can be accessed later on from
+ // the returned object.
+ editor.env = env;
+ return editor;
+};
+
+});
+// vim:set ts=4 sts=4 sw=4 st:
+// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License
+// -- tlrobinson Tom Robinson Copyright (C) 2009-2010 MIT License (Narwhal Project)
+// -- dantman Daniel Friesen Copyright(C) 2010 XXX No License Specified
+// -- fschaefer Florian Schäfer Copyright (C) 2010 MIT License
+// -- Irakli Gozalishvili Copyright (C) 2010 MIT License
+
+/*!
+ Copyright (c) 2009, 280 North Inc. http://280north.com/
+ MIT License. http://github.com/280north/narwhal/blob/master/README.md
+*/
+
+ace.define('ace/lib/fixoldbrowsers', ['require', 'exports', 'module' , 'ace/lib/regexp', 'ace/lib/es5-shim'], function(require, exports, module) {
+
+
+require("./regexp");
+require("./es5-shim");
+
+});
+
+ace.define('ace/lib/regexp', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+ //---------------------------------
+ // Private variables
+ //---------------------------------
+
+ var real = {
+ exec: RegExp.prototype.exec,
+ test: RegExp.prototype.test,
+ match: String.prototype.match,
+ replace: String.prototype.replace,
+ split: String.prototype.split
+ },
+ compliantExecNpcg = real.exec.call(/()??/, "")[1] === undefined, // check `exec` handling of nonparticipating capturing groups
+ compliantLastIndexIncrement = function () {
+ var x = /^/g;
+ real.test.call(x, "");
+ return !x.lastIndex;
+ }();
+
+ if (compliantLastIndexIncrement && compliantExecNpcg)
+ return;
+
+ //---------------------------------
+ // Overriden native methods
+ //---------------------------------
+
+ // Adds named capture support (with backreferences returned as `result.name`), and fixes two
+ // cross-browser issues per ES3:
+ // - Captured values for nonparticipating capturing groups should be returned as `undefined`,
+ // rather than the empty string.
+ // - `lastIndex` should not be incremented after zero-length matches.
+ RegExp.prototype.exec = function (str) {
+ var match = real.exec.apply(this, arguments),
+ name, r2;
+ if ( typeof(str) == 'string' && match) {
+ // Fix browsers whose `exec` methods don't consistently return `undefined` for
+ // nonparticipating capturing groups
+ if (!compliantExecNpcg && match.length > 1 && indexOf(match, "") > -1) {
+ r2 = RegExp(this.source, real.replace.call(getNativeFlags(this), "g", ""));
+ // Using `str.slice(match.index)` rather than `match[0]` in case lookahead allowed
+ // matching due to characters outside the match
+ real.replace.call(str.slice(match.index), r2, function () {
+ for (var i = 1; i < arguments.length - 2; i++) {
+ if (arguments[i] === undefined)
+ match[i] = undefined;
+ }
+ });
+ }
+ // Attach named capture properties
+ if (this._xregexp && this._xregexp.captureNames) {
+ for (var i = 1; i < match.length; i++) {
+ name = this._xregexp.captureNames[i - 1];
+ if (name)
+ match[name] = match[i];
+ }
+ }
+ // Fix browsers that increment `lastIndex` after zero-length matches
+ if (!compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index))
+ this.lastIndex--;
+ }
+ return match;
+ };
+
+ // Don't override `test` if it won't change anything
+ if (!compliantLastIndexIncrement) {
+ // Fix browser bug in native method
+ RegExp.prototype.test = function (str) {
+ // Use the native `exec` to skip some processing overhead, even though the overriden
+ // `exec` would take care of the `lastIndex` fix
+ var match = real.exec.call(this, str);
+ // Fix browsers that increment `lastIndex` after zero-length matches
+ if (match && this.global && !match[0].length && (this.lastIndex > match.index))
+ this.lastIndex--;
+ return !!match;
+ };
+ }
+
+ //---------------------------------
+ // Private helper functions
+ //---------------------------------
+
+ function getNativeFlags (regex) {
+ return (regex.global ? "g" : "") +
+ (regex.ignoreCase ? "i" : "") +
+ (regex.multiline ? "m" : "") +
+ (regex.extended ? "x" : "") + // Proposed for ES4; included in AS3
+ (regex.sticky ? "y" : "");
+ }
+
+ function indexOf (array, item, from) {
+ if (Array.prototype.indexOf) // Use the native array method if available
+ return array.indexOf(item, from);
+ for (var i = from || 0; i < array.length; i++) {
+ if (array[i] === item)
+ return i;
+ }
+ return -1;
+ }
+
+});
+// vim: ts=4 sts=4 sw=4 expandtab
+// -- kriskowal Kris Kowal Copyright (C) 2009-2011 MIT License
+// -- tlrobinson Tom Robinson Copyright (C) 2009-2010 MIT License (Narwhal Project)
+// -- dantman Daniel Friesen Copyright (C) 2010 XXX TODO License or CLA
+// -- fschaefer Florian Schäfer Copyright (C) 2010 MIT License
+// -- Gozala Irakli Gozalishvili Copyright (C) 2010 MIT License
+// -- kitcambridge Kit Cambridge Copyright (C) 2011 MIT License
+// -- kossnocorp Sasha Koss XXX TODO License or CLA
+// -- bryanforbes Bryan Forbes XXX TODO License or CLA
+// -- killdream Quildreen Motta Copyright (C) 2011 MIT Licence
+// -- michaelficarra Michael Ficarra Copyright (C) 2011 3-clause BSD License
+// -- sharkbrainguy Gerard Paapu Copyright (C) 2011 MIT License
+// -- bbqsrc Brendan Molloy (C) 2011 Creative Commons Zero (public domain)
+// -- iwyg XXX TODO License or CLA
+// -- DomenicDenicola Domenic Denicola Copyright (C) 2011 MIT License
+// -- xavierm02 Montillet Xavier XXX TODO License or CLA
+// -- Raynos Raynos XXX TODO License or CLA
+// -- samsonjs Sami Samhuri Copyright (C) 2010 MIT License
+// -- rwldrn Rick Waldron Copyright (C) 2011 MIT License
+// -- lexer Alexey Zakharov XXX TODO License or CLA
+
+/*!
+ Copyright (c) 2009, 280 North Inc. http://280north.com/
+ MIT License. http://github.com/280north/narwhal/blob/master/README.md
+*/
+
+ace.define('ace/lib/es5-shim', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+/*
+ * Brings an environment as close to ECMAScript 5 compliance
+ * as is possible with the facilities of erstwhile engines.
+ *
+ * Annotated ES5: http://es5.github.com/ (specific links below)
+ * ES5 Spec: http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf
+ *
+ * @module
+ */
+
+/*whatsupdoc*/
+
+//
+// Function
+// ========
+//
+
+// ES-5 15.3.4.5
+// http://es5.github.com/#x15.3.4.5
+
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function bind(that) { // .length is 1
+ // 1. Let Target be the this value.
+ var target = this;
+ // 2. If IsCallable(Target) is false, throw a TypeError exception.
+ if (typeof target != "function")
+ throw new TypeError(); // TODO message
+ // 3. Let A be a new (possibly empty) internal list of all of the
+ // argument values provided after thisArg (arg1, arg2 etc), in order.
+ // XXX slicedArgs will stand in for "A" if used
+ var args = slice.call(arguments, 1); // for normal call
+ // 4. Let F be a new native ECMAScript object.
+ // 11. Set the [[Prototype]] internal property of F to the standard
+ // built-in Function prototype object as specified in 15.3.3.1.
+ // 12. Set the [[Call]] internal property of F as described in
+ // 15.3.4.5.1.
+ // 13. Set the [[Construct]] internal property of F as described in
+ // 15.3.4.5.2.
+ // 14. Set the [[HasInstance]] internal property of F as described in
+ // 15.3.4.5.3.
+ var bound = function () {
+
+ if (this instanceof bound) {
+ // 15.3.4.5.2 [[Construct]]
+ // When the [[Construct]] internal method of a function object,
+ // F that was created using the bind function is called with a
+ // list of arguments ExtraArgs, the following steps are taken:
+ // 1. Let target be the value of F's [[TargetFunction]]
+ // internal property.
+ // 2. If target has no [[Construct]] internal method, a
+ // TypeError exception is thrown.
+ // 3. Let boundArgs be the value of F's [[BoundArgs]] internal
+ // property.
+ // 4. Let args be a new list containing the same values as the
+ // list boundArgs in the same order followed by the same
+ // values as the list ExtraArgs in the same order.
+ // 5. Return the result of calling the [[Construct]] internal
+ // method of target providing args as the arguments.
+
+ var F = function(){};
+ F.prototype = target.prototype;
+ var self = new F;
+
+ var result = target.apply(
+ self,
+ args.concat(slice.call(arguments))
+ );
+ if (result !== null && Object(result) === result)
+ return result;
+ return self;
+
+ } else {
+ // 15.3.4.5.1 [[Call]]
+ // When the [[Call]] internal method of a function object, F,
+ // which was created using the bind function is called with a
+ // this value and a list of arguments ExtraArgs, the following
+ // steps are taken:
+ // 1. Let boundArgs be the value of F's [[BoundArgs]] internal
+ // property.
+ // 2. Let boundThis be the value of F's [[BoundThis]] internal
+ // property.
+ // 3. Let target be the value of F's [[TargetFunction]] internal
+ // property.
+ // 4. Let args be a new list containing the same values as the
+ // list boundArgs in the same order followed by the same
+ // values as the list ExtraArgs in the same order.
+ // 5. Return the result of calling the [[Call]] internal method
+ // of target providing boundThis as the this value and
+ // providing args as the arguments.
+
+ // equiv: target.call(this, ...boundArgs, ...args)
+ return target.apply(
+ that,
+ args.concat(slice.call(arguments))
+ );
+
+ }
+
+ };
+ // XXX bound.length is never writable, so don't even try
+ //
+ // 15. If the [[Class]] internal property of Target is "Function", then
+ // a. Let L be the length property of Target minus the length of A.
+ // b. Set the length own property of F to either 0 or L, whichever is
+ // larger.
+ // 16. Else set the length own property of F to 0.
+ // 17. Set the attributes of the length own property of F to the values
+ // specified in 15.3.5.1.
+
+ // TODO
+ // 18. Set the [[Extensible]] internal property of F to true.
+
+ // TODO
+ // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3).
+ // 20. Call the [[DefineOwnProperty]] internal method of F with
+ // arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]:
+ // thrower, [[Enumerable]]: false, [[Configurable]]: false}, and
+ // false.
+ // 21. Call the [[DefineOwnProperty]] internal method of F with
+ // arguments "arguments", PropertyDescriptor {[[Get]]: thrower,
+ // [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},
+ // and false.
+
+ // TODO
+ // NOTE Function objects created using Function.prototype.bind do not
+ // have a prototype property or the [[Code]], [[FormalParameters]], and
+ // [[Scope]] internal properties.
+ // XXX can't delete prototype in pure-js.
+
+ // 22. Return F.
+ return bound;
+ };
+}
+
+// Shortcut to an often accessed properties, in order to avoid multiple
+// dereference that costs universally.
+// _Please note: Shortcuts are defined after `Function.prototype.bind` as we
+// us it in defining shortcuts.
+var call = Function.prototype.call;
+var prototypeOfArray = Array.prototype;
+var prototypeOfObject = Object.prototype;
+var slice = prototypeOfArray.slice;
+var toString = call.bind(prototypeOfObject.toString);
+var owns = call.bind(prototypeOfObject.hasOwnProperty);
+
+// If JS engine supports accessors creating shortcuts.
+var defineGetter;
+var defineSetter;
+var lookupGetter;
+var lookupSetter;
+var supportsAccessors;
+if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) {
+ defineGetter = call.bind(prototypeOfObject.__defineGetter__);
+ defineSetter = call.bind(prototypeOfObject.__defineSetter__);
+ lookupGetter = call.bind(prototypeOfObject.__lookupGetter__);
+ lookupSetter = call.bind(prototypeOfObject.__lookupSetter__);
+}
+
+//
+// Array
+// =====
+//
+
+// ES5 15.4.3.2
+// http://es5.github.com/#x15.4.3.2
+// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray
+if (!Array.isArray) {
+ Array.isArray = function isArray(obj) {
+ return toString(obj) == "[object Array]";
+ };
+}
+
+// The IsCallable() check in the Array functions
+// has been replaced with a strict check on the
+// internal class of the object to trap cases where
+// the provided function was actually a regular
+// expression literal, which in V8 and
+// JavaScriptCore is a typeof "function". Only in
+// V8 are regular expression literals permitted as
+// reduce parameters, so it is desirable in the
+// general case for the shim to match the more
+// strict and common behavior of rejecting regular
+// expressions.
+
+// ES5 15.4.4.18
+// http://es5.github.com/#x15.4.4.18
+// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/forEach
+if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function forEach(fun /*, thisp*/) {
+ var self = toObject(this),
+ thisp = arguments[1],
+ i = 0,
+ length = self.length >>> 0;
+
+ // If no callback function or if callback is not a callable function
+ if (toString(fun) != "[object Function]") {
+ throw new TypeError(); // TODO message
+ }
+
+ while (i < length) {
+ if (i in self) {
+ // Invoke the callback function with call, passing arguments:
+ // context, property value, property key, thisArg object context
+ fun.call(thisp, self[i], i, self);
+ }
+ i++;
+ }
+ };
+}
+
+// ES5 15.4.4.19
+// http://es5.github.com/#x15.4.4.19
+// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map
+if (!Array.prototype.map) {
+ Array.prototype.map = function map(fun /*, thisp*/) {
+ var self = toObject(this),
+ length = self.length >>> 0,
+ result = Array(length),
+ thisp = arguments[1];
+
+ // If no callback function or if callback is not a callable function
+ if (toString(fun) != "[object Function]") {
+ throw new TypeError(); // TODO message
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (i in self)
+ result[i] = fun.call(thisp, self[i], i, self);
+ }
+ return result;
+ };
+}
+
+// ES5 15.4.4.20
+// http://es5.github.com/#x15.4.4.20
+// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/filter
+if (!Array.prototype.filter) {
+ Array.prototype.filter = function filter(fun /*, thisp */) {
+ var self = toObject(this),
+ length = self.length >>> 0,
+ result = [],
+ thisp = arguments[1];
+
+ // If no callback function or if callback is not a callable function
+ if (toString(fun) != "[object Function]") {
+ throw new TypeError(); // TODO message
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (i in self && fun.call(thisp, self[i], i, self))
+ result.push(self[i]);
+ }
+ return result;
+ };
+}
+
+// ES5 15.4.4.16
+// http://es5.github.com/#x15.4.4.16
+// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every
+if (!Array.prototype.every) {
+ Array.prototype.every = function every(fun /*, thisp */) {
+ var self = toObject(this),
+ length = self.length >>> 0,
+ thisp = arguments[1];
+
+ // If no callback function or if callback is not a callable function
+ if (toString(fun) != "[object Function]") {
+ throw new TypeError(); // TODO message
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (i in self && !fun.call(thisp, self[i], i, self))
+ return false;
+ }
+ return true;
+ };
+}
+
+// ES5 15.4.4.17
+// http://es5.github.com/#x15.4.4.17
+// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some
+if (!Array.prototype.some) {
+ Array.prototype.some = function some(fun /*, thisp */) {
+ var self = toObject(this),
+ length = self.length >>> 0,
+ thisp = arguments[1];
+
+ // If no callback function or if callback is not a callable function
+ if (toString(fun) != "[object Function]") {
+ throw new TypeError(); // TODO message
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (i in self && fun.call(thisp, self[i], i, self))
+ return true;
+ }
+ return false;
+ };
+}
+
+// ES5 15.4.4.21
+// http://es5.github.com/#x15.4.4.21
+// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduce
+if (!Array.prototype.reduce) {
+ Array.prototype.reduce = function reduce(fun /*, initial*/) {
+ var self = toObject(this),
+ length = self.length >>> 0;
+
+ // If no callback function or if callback is not a callable function
+ if (toString(fun) != "[object Function]") {
+ throw new TypeError(); // TODO message
+ }
+
+ // no value to return if no initial value and an empty array
+ if (!length && arguments.length == 1)
+ throw new TypeError(); // TODO message
+
+ var i = 0;
+ var result;
+ if (arguments.length >= 2) {
+ result = arguments[1];
+ } else {
+ do {
+ if (i in self) {
+ result = self[i++];
+ break;
+ }
+
+ // if array contains no values, no initial value to return
+ if (++i >= length)
+ throw new TypeError(); // TODO message
+ } while (true);
+ }
+
+ for (; i < length; i++) {
+ if (i in self)
+ result = fun.call(void 0, result, self[i], i, self);
+ }
+
+ return result;
+ };
+}
+
+// ES5 15.4.4.22
+// http://es5.github.com/#x15.4.4.22
+// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduceRight
+if (!Array.prototype.reduceRight) {
+ Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) {
+ var self = toObject(this),
+ length = self.length >>> 0;
+
+ // If no callback function or if callback is not a callable function
+ if (toString(fun) != "[object Function]") {
+ throw new TypeError(); // TODO message
+ }
+
+ // no value to return if no initial value, empty array
+ if (!length && arguments.length == 1)
+ throw new TypeError(); // TODO message
+
+ var result, i = length - 1;
+ if (arguments.length >= 2) {
+ result = arguments[1];
+ } else {
+ do {
+ if (i in self) {
+ result = self[i--];
+ break;
+ }
+
+ // if array contains no values, no initial value to return
+ if (--i < 0)
+ throw new TypeError(); // TODO message
+ } while (true);
+ }
+
+ do {
+ if (i in this)
+ result = fun.call(void 0, result, self[i], i, self);
+ } while (i--);
+
+ return result;
+ };
+}
+
+// ES5 15.4.4.14
+// http://es5.github.com/#x15.4.4.14
+// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
+if (!Array.prototype.indexOf) {
+ Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) {
+ var self = toObject(this),
+ length = self.length >>> 0;
+
+ if (!length)
+ return -1;
+
+ var i = 0;
+ if (arguments.length > 1)
+ i = toInteger(arguments[1]);
+
+ // handle negative indices
+ i = i >= 0 ? i : Math.max(0, length + i);
+ for (; i < length; i++) {
+ if (i in self && self[i] === sought) {
+ return i;
+ }
+ }
+ return -1;
+ };
+}
+
+// ES5 15.4.4.15
+// http://es5.github.com/#x15.4.4.15
+// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf
+if (!Array.prototype.lastIndexOf) {
+ Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) {
+ var self = toObject(this),
+ length = self.length >>> 0;
+
+ if (!length)
+ return -1;
+ var i = length - 1;
+ if (arguments.length > 1)
+ i = Math.min(i, toInteger(arguments[1]));
+ // handle negative indices
+ i = i >= 0 ? i : length - Math.abs(i);
+ for (; i >= 0; i--) {
+ if (i in self && sought === self[i])
+ return i;
+ }
+ return -1;
+ };
+}
+
+//
+// Object
+// ======
+//
+
+// ES5 15.2.3.2
+// http://es5.github.com/#x15.2.3.2
+if (!Object.getPrototypeOf) {
+ // https://github.com/kriskowal/es5-shim/issues#issue/2
+ // http://ejohn.org/blog/objectgetprototypeof/
+ // recommended by fschaefer on github
+ Object.getPrototypeOf = function getPrototypeOf(object) {
+ return object.__proto__ || (
+ object.constructor ?
+ object.constructor.prototype :
+ prototypeOfObject
+ );
+ };
+}
+
+// ES5 15.2.3.3
+// http://es5.github.com/#x15.2.3.3
+if (!Object.getOwnPropertyDescriptor) {
+ var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " +
+ "non-object: ";
+ Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) {
+ if ((typeof object != "object" && typeof object != "function") || object === null)
+ throw new TypeError(ERR_NON_OBJECT + object);
+ // If object does not owns property return undefined immediately.
+ if (!owns(object, property))
+ return;
+
+ var descriptor, getter, setter;
+
+ // If object has a property then it's for sure both `enumerable` and
+ // `configurable`.
+ descriptor = { enumerable: true, configurable: true };
+
+ // If JS engine supports accessor properties then property may be a
+ // getter or setter.
+ if (supportsAccessors) {
+ // Unfortunately `__lookupGetter__` will return a getter even
+ // if object has own non getter property along with a same named
+ // inherited getter. To avoid misbehavior we temporary remove
+ // `__proto__` so that `__lookupGetter__` will return getter only
+ // if it's owned by an object.
+ var prototype = object.__proto__;
+ object.__proto__ = prototypeOfObject;
+
+ var getter = lookupGetter(object, property);
+ var setter = lookupSetter(object, property);
+
+ // Once we have getter and setter we can put values back.
+ object.__proto__ = prototype;
+
+ if (getter || setter) {
+ if (getter) descriptor.get = getter;
+ if (setter) descriptor.set = setter;
+
+ // If it was accessor property we're done and return here
+ // in order to avoid adding `value` to the descriptor.
+ return descriptor;
+ }
+ }
+
+ // If we got this far we know that object has an own property that is
+ // not an accessor so we set it as a value and return descriptor.
+ descriptor.value = object[property];
+ return descriptor;
+ };
+}
+
+// ES5 15.2.3.4
+// http://es5.github.com/#x15.2.3.4
+if (!Object.getOwnPropertyNames) {
+ Object.getOwnPropertyNames = function getOwnPropertyNames(object) {
+ return Object.keys(object);
+ };
+}
+
+// ES5 15.2.3.5
+// http://es5.github.com/#x15.2.3.5
+if (!Object.create) {
+ Object.create = function create(prototype, properties) {
+ var object;
+ if (prototype === null) {
+ object = { "__proto__": null };
+ } else {
+ if (typeof prototype != "object")
+ throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'");
+ var Type = function () {};
+ Type.prototype = prototype;
+ object = new Type();
+ // IE has no built-in implementation of `Object.getPrototypeOf`
+ // neither `__proto__`, but this manually setting `__proto__` will
+ // guarantee that `Object.getPrototypeOf` will work as expected with
+ // objects created using `Object.create`
+ object.__proto__ = prototype;
+ }
+ if (properties !== void 0)
+ Object.defineProperties(object, properties);
+ return object;
+ };
+}
+
+// ES5 15.2.3.6
+// http://es5.github.com/#x15.2.3.6
+
+// Patch for WebKit and IE8 standard mode
+// Designed by hax
+// related issue: https://github.com/kriskowal/es5-shim/issues#issue/5
+// IE8 Reference:
+// http://msdn.microsoft.com/en-us/library/dd282900.aspx
+// http://msdn.microsoft.com/en-us/library/dd229916.aspx
+// WebKit Bugs:
+// https://bugs.webkit.org/show_bug.cgi?id=36423
+
+function doesDefinePropertyWork(object) {
+ try {
+ Object.defineProperty(object, "sentinel", {});
+ return "sentinel" in object;
+ } catch (exception) {
+ // returns falsy
+ }
+}
+
+// check whether defineProperty works if it's given. Otherwise,
+// shim partially.
+if (Object.defineProperty) {
+ var definePropertyWorksOnObject = doesDefinePropertyWork({});
+ var definePropertyWorksOnDom = typeof document == "undefined" ||
+ doesDefinePropertyWork(document.createElement("div"));
+ if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) {
+ var definePropertyFallback = Object.defineProperty;
+ }
+}
+
+if (!Object.defineProperty || definePropertyFallback) {
+ var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: ";
+ var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: "
+ var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " +
+ "on this javascript engine";
+
+ Object.defineProperty = function defineProperty(object, property, descriptor) {
+ if ((typeof object != "object" && typeof object != "function") || object === null)
+ throw new TypeError(ERR_NON_OBJECT_TARGET + object);
+ if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null)
+ throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);
+
+ // make a valiant attempt to use the real defineProperty
+ // for I8's DOM elements.
+ if (definePropertyFallback) {
+ try {
+ return definePropertyFallback.call(Object, object, property, descriptor);
+ } catch (exception) {
+ // try the shim if the real one doesn't work
+ }
+ }
+
+ // If it's a data property.
+ if (owns(descriptor, "value")) {
+ // fail silently if "writable", "enumerable", or "configurable"
+ // are requested but not supported
+ /*
+ // alternate approach:
+ if ( // can't implement these features; allow false but not true
+ !(owns(descriptor, "writable") ? descriptor.writable : true) ||
+ !(owns(descriptor, "enumerable") ? descriptor.enumerable : true) ||
+ !(owns(descriptor, "configurable") ? descriptor.configurable : true)
+ )
+ throw new RangeError(
+ "This implementation of Object.defineProperty does not " +
+ "support configurable, enumerable, or writable."
+ );
+ */
+
+ if (supportsAccessors && (lookupGetter(object, property) ||
+ lookupSetter(object, property)))
+ {
+ // As accessors are supported only on engines implementing
+ // `__proto__` we can safely override `__proto__` while defining
+ // a property to make sure that we don't hit an inherited
+ // accessor.
+ var prototype = object.__proto__;
+ object.__proto__ = prototypeOfObject;
+ // Deleting a property anyway since getter / setter may be
+ // defined on object itself.
+ delete object[property];
+ object[property] = descriptor.value;
+ // Setting original `__proto__` back now.
+ object.__proto__ = prototype;
+ } else {
+ object[property] = descriptor.value;
+ }
+ } else {
+ if (!supportsAccessors)
+ throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);
+ // If we got that far then getters and setters can be defined !!
+ if (owns(descriptor, "get"))
+ defineGetter(object, property, descriptor.get);
+ if (owns(descriptor, "set"))
+ defineSetter(object, property, descriptor.set);
+ }
+
+ return object;
+ };
+}
+
+// ES5 15.2.3.7
+// http://es5.github.com/#x15.2.3.7
+if (!Object.defineProperties) {
+ Object.defineProperties = function defineProperties(object, properties) {
+ for (var property in properties) {
+ if (owns(properties, property))
+ Object.defineProperty(object, property, properties[property]);
+ }
+ return object;
+ };
+}
+
+// ES5 15.2.3.8
+// http://es5.github.com/#x15.2.3.8
+if (!Object.seal) {
+ Object.seal = function seal(object) {
+ // this is misleading and breaks feature-detection, but
+ // allows "securable" code to "gracefully" degrade to working
+ // but insecure code.
+ return object;
+ };
+}
+
+// ES5 15.2.3.9
+// http://es5.github.com/#x15.2.3.9
+if (!Object.freeze) {
+ Object.freeze = function freeze(object) {
+ // this is misleading and breaks feature-detection, but
+ // allows "securable" code to "gracefully" degrade to working
+ // but insecure code.
+ return object;
+ };
+}
+
+// detect a Rhino bug and patch it
+try {
+ Object.freeze(function () {});
+} catch (exception) {
+ Object.freeze = (function freeze(freezeObject) {
+ return function freeze(object) {
+ if (typeof object == "function") {
+ return object;
+ } else {
+ return freezeObject(object);
+ }
+ };
+ })(Object.freeze);
+}
+
+// ES5 15.2.3.10
+// http://es5.github.com/#x15.2.3.10
+if (!Object.preventExtensions) {
+ Object.preventExtensions = function preventExtensions(object) {
+ // this is misleading and breaks feature-detection, but
+ // allows "securable" code to "gracefully" degrade to working
+ // but insecure code.
+ return object;
+ };
+}
+
+// ES5 15.2.3.11
+// http://es5.github.com/#x15.2.3.11
+if (!Object.isSealed) {
+ Object.isSealed = function isSealed(object) {
+ return false;
+ };
+}
+
+// ES5 15.2.3.12
+// http://es5.github.com/#x15.2.3.12
+if (!Object.isFrozen) {
+ Object.isFrozen = function isFrozen(object) {
+ return false;
+ };
+}
+
+// ES5 15.2.3.13
+// http://es5.github.com/#x15.2.3.13
+if (!Object.isExtensible) {
+ Object.isExtensible = function isExtensible(object) {
+ // 1. If Type(O) is not Object throw a TypeError exception.
+ if (Object(object) === object) {
+ throw new TypeError(); // TODO message
+ }
+ // 2. Return the Boolean value of the [[Extensible]] internal property of O.
+ var name = '';
+ while (owns(object, name)) {
+ name += '?';
+ }
+ object[name] = true;
+ var returnValue = owns(object, name);
+ delete object[name];
+ return returnValue;
+ };
+}
+
+// ES5 15.2.3.14
+// http://es5.github.com/#x15.2.3.14
+if (!Object.keys) {
+ // http://whattheheadsaid.com/2010/10/a-safer-object-keys-compatibility-implementation
+ var hasDontEnumBug = true,
+ dontEnums = [
+ "toString",
+ "toLocaleString",
+ "valueOf",
+ "hasOwnProperty",
+ "isPrototypeOf",
+ "propertyIsEnumerable",
+ "constructor"
+ ],
+ dontEnumsLength = dontEnums.length;
+
+ for (var key in {"toString": null})
+ hasDontEnumBug = false;
+
+ Object.keys = function keys(object) {
+
+ if ((typeof object != "object" && typeof object != "function") || object === null)
+ throw new TypeError("Object.keys called on a non-object");
+
+ var keys = [];
+ for (var name in object) {
+ if (owns(object, name)) {
+ keys.push(name);
+ }
+ }
+
+ if (hasDontEnumBug) {
+ for (var i = 0, ii = dontEnumsLength; i < ii; i++) {
+ var dontEnum = dontEnums[i];
+ if (owns(object, dontEnum)) {
+ keys.push(dontEnum);
+ }
+ }
+ }
+
+ return keys;
+ };
+
+}
+
+//
+// Date
+// ====
+//
+
+// ES5 15.9.5.43
+// http://es5.github.com/#x15.9.5.43
+// This function returns a String value represent the instance in time
+// represented by this Date object. The format of the String is the Date Time
+// string format defined in 15.9.1.15. All fields are present in the String.
+// The time zone is always UTC, denoted by the suffix Z. If the time value of
+// this object is not a finite Number a RangeError exception is thrown.
+if (!Date.prototype.toISOString || (new Date(-62198755200000).toISOString().indexOf('-000001') === -1)) {
+ Date.prototype.toISOString = function toISOString() {
+ var result, length, value, year;
+ if (!isFinite(this))
+ throw new RangeError;
+
+ // the date time string format is specified in 15.9.1.15.
+ result = [this.getUTCMonth() + 1, this.getUTCDate(),
+ this.getUTCHours(), this.getUTCMinutes(), this.getUTCSeconds()];
+ year = this.getUTCFullYear();
+ year = (year < 0 ? '-' : (year > 9999 ? '+' : '')) + ('00000' + Math.abs(year)).slice(0 <= year && year <= 9999 ? -4 : -6);
+
+ length = result.length;
+ while (length--) {
+ value = result[length];
+ // pad months, days, hours, minutes, and seconds to have two digits.
+ if (value < 10)
+ result[length] = "0" + value;
+ }
+ // pad milliseconds to have three digits.
+ return year + "-" + result.slice(0, 2).join("-") + "T" + result.slice(2).join(":") + "." +
+ ("000" + this.getUTCMilliseconds()).slice(-3) + "Z";
+ }
+}
+
+// ES5 15.9.4.4
+// http://es5.github.com/#x15.9.4.4
+if (!Date.now) {
+ Date.now = function now() {
+ return new Date().getTime();
+ };
+}
+
+// ES5 15.9.5.44
+// http://es5.github.com/#x15.9.5.44
+// This function provides a String representation of a Date object for use by
+// JSON.stringify (15.12.3).
+if (!Date.prototype.toJSON) {
+ Date.prototype.toJSON = function toJSON(key) {
+ // When the toJSON method is called with argument key, the following
+ // steps are taken:
+
+ // 1. Let O be the result of calling ToObject, giving it the this
+ // value as its argument.
+ // 2. Let tv be ToPrimitive(O, hint Number).
+ // 3. If tv is a Number and is not finite, return null.
+ // XXX
+ // 4. Let toISO be the result of calling the [[Get]] internal method of
+ // O with argument "toISOString".
+ // 5. If IsCallable(toISO) is false, throw a TypeError exception.
+ if (typeof this.toISOString != "function")
+ throw new TypeError(); // TODO message
+ // 6. Return the result of calling the [[Call]] internal method of
+ // toISO with O as the this value and an empty argument list.
+ return this.toISOString();
+
+ // NOTE 1 The argument is ignored.
+
+ // NOTE 2 The toJSON function is intentionally generic; it does not
+ // require that its this value be a Date object. Therefore, it can be
+ // transferred to other kinds of objects for use as a method. However,
+ // it does require that any such object have a toISOString method. An
+ // object is free to use the argument key to filter its
+ // stringification.
+ };
+}
+
+// ES5 15.9.4.2
+// http://es5.github.com/#x15.9.4.2
+// based on work shared by Daniel Friesen (dantman)
+// http://gist.github.com/303249
+if (Date.parse("+275760-09-13T00:00:00.000Z") !== 8.64e15) {
+ // XXX global assignment won't work in embeddings that use
+ // an alternate object for the context.
+ Date = (function(NativeDate) {
+
+ // Date.length === 7
+ var Date = function Date(Y, M, D, h, m, s, ms) {
+ var length = arguments.length;
+ if (this instanceof NativeDate) {
+ var date = length == 1 && String(Y) === Y ? // isString(Y)
+ // We explicitly pass it through parse:
+ new NativeDate(Date.parse(Y)) :
+ // We have to manually make calls depending on argument
+ // length here
+ length >= 7 ? new NativeDate(Y, M, D, h, m, s, ms) :
+ length >= 6 ? new NativeDate(Y, M, D, h, m, s) :
+ length >= 5 ? new NativeDate(Y, M, D, h, m) :
+ length >= 4 ? new NativeDate(Y, M, D, h) :
+ length >= 3 ? new NativeDate(Y, M, D) :
+ length >= 2 ? new NativeDate(Y, M) :
+ length >= 1 ? new NativeDate(Y) :
+ new NativeDate();
+ // Prevent mixups with unfixed Date object
+ date.constructor = Date;
+ return date;
+ }
+ return NativeDate.apply(this, arguments);
+ };
+
+ // 15.9.1.15 Date Time String Format.
+ var isoDateExpression = new RegExp("^" +
+ "(\\d{4}|[\+\-]\\d{6})" + // four-digit year capture or sign + 6-digit extended year
+ "(?:-(\\d{2})" + // optional month capture
+ "(?:-(\\d{2})" + // optional day capture
+ "(?:" + // capture hours:minutes:seconds.milliseconds
+ "T(\\d{2})" + // hours capture
+ ":(\\d{2})" + // minutes capture
+ "(?:" + // optional :seconds.milliseconds
+ ":(\\d{2})" + // seconds capture
+ "(?:\\.(\\d{3}))?" + // milliseconds capture
+ ")?" +
+ "(?:" + // capture UTC offset component
+ "Z|" + // UTC capture
+ "(?:" + // offset specifier +/-hours:minutes
+ "([-+])" + // sign capture
+ "(\\d{2})" + // hours offset capture
+ ":(\\d{2})" + // minutes offset capture
+ ")" +
+ ")?)?)?)?" +
+ "$");
+
+ // Copy any custom methods a 3rd party library may have added
+ for (var key in NativeDate)
+ Date[key] = NativeDate[key];
+
+ // Copy "native" methods explicitly; they may be non-enumerable
+ Date.now = NativeDate.now;
+ Date.UTC = NativeDate.UTC;
+ Date.prototype = NativeDate.prototype;
+ Date.prototype.constructor = Date;
+
+ // Upgrade Date.parse to handle simplified ISO 8601 strings
+ Date.parse = function parse(string) {
+ var match = isoDateExpression.exec(string);
+ if (match) {
+ match.shift(); // kill match[0], the full match
+ // parse months, days, hours, minutes, seconds, and milliseconds
+ for (var i = 1; i < 7; i++) {
+ // provide default values if necessary
+ match[i] = +(match[i] || (i < 3 ? 1 : 0));
+ // match[1] is the month. Months are 0-11 in JavaScript
+ // `Date` objects, but 1-12 in ISO notation, so we
+ // decrement.
+ if (i == 1)
+ match[i]--;
+ }
+
+ // parse the UTC offset component
+ var minuteOffset = +match.pop(), hourOffset = +match.pop(), sign = match.pop();
+
+ // compute the explicit time zone offset if specified
+ var offset = 0;
+ if (sign) {
+ // detect invalid offsets and return early
+ if (hourOffset > 23 || minuteOffset > 59)
+ return NaN;
+
+ // express the provided time zone offset in minutes. The offset is
+ // negative for time zones west of UTC; positive otherwise.
+ offset = (hourOffset * 60 + minuteOffset) * 6e4 * (sign == "+" ? -1 : 1);
+ }
+
+ // Date.UTC for years between 0 and 99 converts year to 1900 + year
+ // The Gregorian calendar has a 400-year cycle, so
+ // to Date.UTC(year + 400, .... ) - 12622780800000 == Date.UTC(year, ...),
+ // where 12622780800000 - number of milliseconds in Gregorian calendar 400 years
+ var year = +match[0];
+ if (0 <= year && year <= 99) {
+ match[0] = year + 400;
+ return NativeDate.UTC.apply(this, match) + offset - 12622780800000;
+ }
+
+ // compute a new UTC date value, accounting for the optional offset
+ return NativeDate.UTC.apply(this, match) + offset;
+ }
+ return NativeDate.parse.apply(this, arguments);
+ };
+
+ return Date;
+ })(Date);
+}
+
+//
+// String
+// ======
+//
+
+// ES5 15.5.4.20
+// http://es5.github.com/#x15.5.4.20
+var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" +
+ "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" +
+ "\u2029\uFEFF";
+if (!String.prototype.trim || ws.trim()) {
+ // http://blog.stevenlevithan.com/archives/faster-trim-javascript
+ // http://perfectionkills.com/whitespace-deviations/
+ ws = "[" + ws + "]";
+ var trimBeginRegexp = new RegExp("^" + ws + ws + "*"),
+ trimEndRegexp = new RegExp(ws + ws + "*$");
+ String.prototype.trim = function trim() {
+ return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, "");
+ };
+}
+
+//
+// Util
+// ======
+//
+
+// ES5 9.4
+// http://es5.github.com/#x9.4
+// http://jsperf.com/to-integer
+var toInteger = function (n) {
+ n = +n;
+ if (n !== n) // isNaN
+ n = 0;
+ else if (n !== 0 && n !== (1/0) && n !== -(1/0))
+ n = (n > 0 || -1) * Math.floor(Math.abs(n));
+ return n;
+};
+
+var prepareString = "a"[0] != "a",
+ // ES5 9.9
+ // http://es5.github.com/#x9.9
+ toObject = function (o) {
+ if (o == null) { // this matches both null and undefined
+ throw new TypeError(); // TODO message
+ }
+ // If the implementation doesn't support by-index access of
+ // string characters (ex. IE < 7), split the string
+ if (prepareString && typeof o == "string" && o) {
+ return o.split("");
+ }
+ return Object(o);
+ };
+});
+
+ace.define('ace/lib/dom', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+var XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+exports.createElement = function(tag, ns) {
+ return document.createElementNS ?
+ document.createElementNS(ns || XHTML_NS, tag) :
+ document.createElement(tag);
+};
+
+exports.setText = function(elem, text) {
+ if (elem.innerText !== undefined) {
+ elem.innerText = text;
+ }
+ if (elem.textContent !== undefined) {
+ elem.textContent = text;
+ }
+};
+
+exports.hasCssClass = function(el, name) {
+ var classes = el.className.split(/\s+/g);
+ return classes.indexOf(name) !== -1;
+};
+exports.addCssClass = function(el, name) {
+ if (!exports.hasCssClass(el, name)) {
+ el.className += " " + name;
+ }
+};
+exports.removeCssClass = function(el, name) {
+ var classes = el.className.split(/\s+/g);
+ while (true) {
+ var index = classes.indexOf(name);
+ if (index == -1) {
+ break;
+ }
+ classes.splice(index, 1);
+ }
+ el.className = classes.join(" ");
+};
+
+exports.toggleCssClass = function(el, name) {
+ var classes = el.className.split(/\s+/g), add = true;
+ while (true) {
+ var index = classes.indexOf(name);
+ if (index == -1) {
+ break;
+ }
+ add = false;
+ classes.splice(index, 1);
+ }
+ if(add)
+ classes.push(name);
+
+ el.className = classes.join(" ");
+ return add;
+};
+exports.setCssClass = function(node, className, include) {
+ if (include) {
+ exports.addCssClass(node, className);
+ } else {
+ exports.removeCssClass(node, className);
+ }
+};
+
+exports.hasCssString = function(id, doc) {
+ var index = 0, sheets;
+ doc = doc || document;
+
+ if (doc.createStyleSheet && (sheets = doc.styleSheets)) {
+ while (index < sheets.length)
+ if (sheets[index++].owningElement.id === id) return true;
+ } else if ((sheets = doc.getElementsByTagName("style"))) {
+ while (index < sheets.length)
+ if (sheets[index++].id === id) return true;
+ }
+
+ return false;
+};
+
+exports.importCssString = function importCssString(cssText, id, doc) {
+ doc = doc || document;
+ // If style is already imported return immediately.
+ if (id && exports.hasCssString(id, doc))
+ return null;
+
+ var style;
+
+ if (doc.createStyleSheet) {
+ style = doc.createStyleSheet();
+ style.cssText = cssText;
+ if (id)
+ style.owningElement.id = id;
+ } else {
+ style = doc.createElementNS
+ ? doc.createElementNS(XHTML_NS, "style")
+ : doc.createElement("style");
+
+ style.appendChild(doc.createTextNode(cssText));
+ if (id)
+ style.id = id;
+
+ var head = doc.getElementsByTagName("head")[0] || doc.documentElement;
+ head.appendChild(style);
+ }
+};
+
+exports.importCssStylsheet = function(uri, doc) {
+ if (doc.createStyleSheet) {
+ doc.createStyleSheet(uri);
+ } else {
+ var link = exports.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = uri;
+
+ var head = doc.getElementsByTagName("head")[0] || doc.documentElement;
+ head.appendChild(link);
+ }
+};
+
+exports.getInnerWidth = function(element) {
+ return (
+ parseInt(exports.computedStyle(element, "paddingLeft"), 10) +
+ parseInt(exports.computedStyle(element, "paddingRight"), 10) +
+ element.clientWidth
+ );
+};
+
+exports.getInnerHeight = function(element) {
+ return (
+ parseInt(exports.computedStyle(element, "paddingTop"), 10) +
+ parseInt(exports.computedStyle(element, "paddingBottom"), 10) +
+ element.clientHeight
+ );
+};
+
+if (window.pageYOffset !== undefined) {
+ exports.getPageScrollTop = function() {
+ return window.pageYOffset;
+ };
+
+ exports.getPageScrollLeft = function() {
+ return window.pageXOffset;
+ };
+}
+else {
+ exports.getPageScrollTop = function() {
+ return document.body.scrollTop;
+ };
+
+ exports.getPageScrollLeft = function() {
+ return document.body.scrollLeft;
+ };
+}
+
+if (window.getComputedStyle)
+ exports.computedStyle = function(element, style) {
+ if (style)
+ return (window.getComputedStyle(element, "") || {})[style] || "";
+ return window.getComputedStyle(element, "") || {};
+ };
+else
+ exports.computedStyle = function(element, style) {
+ if (style)
+ return element.currentStyle[style];
+ return element.currentStyle;
+ };
+
+exports.scrollbarWidth = function(document) {
+
+ var inner = exports.createElement("p");
+ inner.style.width = "100%";
+ inner.style.minWidth = "0px";
+ inner.style.height = "200px";
+
+ var outer = exports.createElement("div");
+ var style = outer.style;
+
+ style.position = "absolute";
+ style.left = "-10000px";
+ style.overflow = "hidden";
+ style.width = "200px";
+ style.minWidth = "0px";
+ style.height = "150px";
+
+ outer.appendChild(inner);
+
+ var body = document.body || document.documentElement;
+ body.appendChild(outer);
+
+ var noScrollbar = inner.offsetWidth;
+
+ style.overflow = "scroll";
+ var withScrollbar = inner.offsetWidth;
+
+ if (noScrollbar == withScrollbar) {
+ withScrollbar = outer.clientWidth;
+ }
+
+ body.removeChild(outer);
+
+ return noScrollbar-withScrollbar;
+};
+exports.setInnerHtml = function(el, innerHtml) {
+ var element = el.cloneNode(false);//document.createElement("div");
+ element.innerHTML = innerHtml;
+ el.parentNode.replaceChild(element, el);
+ return element;
+};
+
+exports.setInnerText = function(el, innerText) {
+ var document = el.ownerDocument;
+ if (document.body && "textContent" in document.body)
+ el.textContent = innerText;
+ else
+ el.innerText = innerText;
+
+};
+
+exports.getInnerText = function(el) {
+ var document = el.ownerDocument;
+ if (document.body && "textContent" in document.body)
+ return el.textContent;
+ else
+ return el.innerText || el.textContent || "";
+};
+
+exports.getParentWindow = function(document) {
+ return document.defaultView || document.parentWindow;
+};
+
+});
+
+ace.define('ace/lib/event', ['require', 'exports', 'module' , 'ace/lib/keys', 'ace/lib/useragent', 'ace/lib/dom'], function(require, exports, module) {
+
+
+var keys = require("./keys");
+var useragent = require("./useragent");
+var dom = require("./dom");
+
+exports.addListener = function(elem, type, callback) {
+ if (elem.addEventListener) {
+ return elem.addEventListener(type, callback, false);
+ }
+ if (elem.attachEvent) {
+ var wrapper = function() {
+ callback(window.event);
+ };
+ callback._wrapper = wrapper;
+ elem.attachEvent("on" + type, wrapper);
+ }
+};
+
+exports.removeListener = function(elem, type, callback) {
+ if (elem.removeEventListener) {
+ return elem.removeEventListener(type, callback, false);
+ }
+ if (elem.detachEvent) {
+ elem.detachEvent("on" + type, callback._wrapper || callback);
+ }
+};
+exports.stopEvent = function(e) {
+ exports.stopPropagation(e);
+ exports.preventDefault(e);
+ return false;
+};
+
+exports.stopPropagation = function(e) {
+ if (e.stopPropagation)
+ e.stopPropagation();
+ else
+ e.cancelBubble = true;
+};
+
+exports.preventDefault = function(e) {
+ if (e.preventDefault)
+ e.preventDefault();
+ else
+ e.returnValue = false;
+};
+exports.getButton = function(e) {
+ if (e.type == "dblclick")
+ return 0;
+ if (e.type == "contextmenu" || (e.ctrlKey && useragent.isMac))
+ return 2;
+
+ // DOM Event
+ if (e.preventDefault) {
+ return e.button;
+ }
+ // old IE
+ else {
+ return {1:0, 2:2, 4:1}[e.button];
+ }
+};
+
+if (document.documentElement.setCapture) {
+ exports.capture = function(el, eventHandler, releaseCaptureHandler) {
+ var called = false;
+ function onReleaseCapture(e) {
+ eventHandler(e);
+
+ if (!called) {
+ called = true;
+ releaseCaptureHandler(e);
+ }
+
+ exports.removeListener(el, "mousemove", eventHandler);
+ exports.removeListener(el, "mouseup", onReleaseCapture);
+ exports.removeListener(el, "losecapture", onReleaseCapture);
+
+ el.releaseCapture();
+ }
+
+ exports.addListener(el, "mousemove", eventHandler);
+ exports.addListener(el, "mouseup", onReleaseCapture);
+ exports.addListener(el, "losecapture", onReleaseCapture);
+ el.setCapture();
+ };
+}
+else {
+ exports.capture = function(el, eventHandler, releaseCaptureHandler) {
+ function onMouseUp(e) {
+ eventHandler && eventHandler(e);
+ releaseCaptureHandler && releaseCaptureHandler(e);
+
+ document.removeEventListener("mousemove", eventHandler, true);
+ document.removeEventListener("mouseup", onMouseUp, true);
+
+ e.stopPropagation();
+ }
+
+ document.addEventListener("mousemove", eventHandler, true);
+ document.addEventListener("mouseup", onMouseUp, true);
+ };
+}
+
+exports.addMouseWheelListener = function(el, callback) {
+ var factor = 8;
+ var listener = function(e) {
+ if (e.wheelDelta !== undefined) {
+ if (e.wheelDeltaX !== undefined) {
+ e.wheelX = -e.wheelDeltaX / factor;
+ e.wheelY = -e.wheelDeltaY / factor;
+ } else {
+ e.wheelX = 0;
+ e.wheelY = -e.wheelDelta / factor;
+ }
+ }
+ else {
+ if (e.axis && e.axis == e.HORIZONTAL_AXIS) {
+ e.wheelX = (e.detail || 0) * 5;
+ e.wheelY = 0;
+ } else {
+ e.wheelX = 0;
+ e.wheelY = (e.detail || 0) * 5;
+ }
+ }
+ callback(e);
+ };
+ exports.addListener(el, "DOMMouseScroll", listener);
+ exports.addListener(el, "mousewheel", listener);
+};
+
+exports.addMultiMouseDownListener = function(el, timeouts, eventHandler, callbackName) {
+ var clicks = 0;
+ var startX, startY, timer;
+ var eventNames = {
+ 2: "dblclick",
+ 3: "tripleclick",
+ 4: "quadclick"
+ };
+
+ exports.addListener(el, "mousedown", function(e) {
+ if (exports.getButton(e) != 0) {
+ clicks = 0;
+ } else {
+ var isNewClick = Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5;
+
+ if (!timer || isNewClick)
+ clicks = 0;
+
+ clicks += 1;
+
+ if (timer)
+ clearTimeout(timer)
+ timer = setTimeout(function() {timer = null}, timeouts[clicks - 1] || 600);
+ }
+ if (clicks == 1) {
+ startX = e.clientX;
+ startY = e.clientY;
+ }
+
+ eventHandler[callbackName]("mousedown", e);
+
+ if (clicks > 4)
+ clicks = 0;
+ else if (clicks > 1)
+ return eventHandler[callbackName](eventNames[clicks], e);
+ });
+
+ if (useragent.isOldIE) {
+ exports.addListener(el, "dblclick", function(e) {
+ clicks = 2;
+ if (timer)
+ clearTimeout(timer);
+ timer = setTimeout(function() {timer = null}, timeouts[clicks - 1] || 600);
+ eventHandler[callbackName]("mousedown", e);
+ eventHandler[callbackName](eventNames[clicks], e);
+ });
+ }
+};
+
+function normalizeCommandKeys(callback, e, keyCode) {
+ var hashId = 0;
+ if ((useragent.isOpera && !("KeyboardEvent" in window)) && useragent.isMac) {
+ hashId = 0 | (e.metaKey ? 1 : 0) | (e.altKey ? 2 : 0)
+ | (e.shiftKey ? 4 : 0) | (e.ctrlKey ? 8 : 0);
+ } else {
+ hashId = 0 | (e.ctrlKey ? 1 : 0) | (e.altKey ? 2 : 0)
+ | (e.shiftKey ? 4 : 0) | (e.metaKey ? 8 : 0);
+ }
+
+ if (keyCode in keys.MODIFIER_KEYS) {
+ switch (keys.MODIFIER_KEYS[keyCode]) {
+ case "Alt":
+ hashId = 2;
+ break;
+ case "Shift":
+ hashId = 4;
+ break;
+ case "Ctrl":
+ hashId = 1;
+ break;
+ default:
+ hashId = 8;
+ break;
+ }
+ keyCode = 0;
+ }
+
+ if (hashId & 8 && (keyCode == 91 || keyCode == 93)) {
+ keyCode = 0;
+ }
+
+ // If there is no hashID and the keyCode is not a function key, then
+ // we don't call the callback as we don't handle a command key here
+ // (it's a normal key/character input).
+ if (!hashId && !(keyCode in keys.FUNCTION_KEYS) && !(keyCode in keys.PRINTABLE_KEYS)) {
+ return false;
+ }
+ return callback(e, hashId, keyCode);
+}
+
+exports.addCommandKeyListener = function(el, callback) {
+ var addListener = exports.addListener;
+ if (useragent.isOldGecko || (useragent.isOpera && !("KeyboardEvent" in window))) {
+ // Old versions of Gecko aka. Firefox < 4.0 didn't repeat the keydown
+ // event if the user pressed the key for a longer time. Instead, the
+ // keydown event was fired once and later on only the keypress event.
+ // To emulate the 'right' keydown behavior, the keyCode of the initial
+ // keyDown event is stored and in the following keypress events the
+ // stores keyCode is used to emulate a keyDown event.
+ var lastKeyDownKeyCode = null;
+ addListener(el, "keydown", function(e) {
+ lastKeyDownKeyCode = e.keyCode;
+ });
+ addListener(el, "keypress", function(e) {
+ return normalizeCommandKeys(callback, e, lastKeyDownKeyCode);
+ });
+ } else {
+ var lastDown = null;
+
+ addListener(el, "keydown", function(e) {
+ lastDown = e.keyIdentifier || e.keyCode;
+ return normalizeCommandKeys(callback, e, e.keyCode);
+ });
+ }
+};
+
+if (window.postMessage && !useragent.isOldIE) {
+ var postMessageId = 1;
+ exports.nextTick = function(callback, win) {
+ win = win || window;
+ var messageName = "zero-timeout-message-" + postMessageId;
+ exports.addListener(win, "message", function listener(e) {
+ if (e.data == messageName) {
+ exports.stopPropagation(e);
+ exports.removeListener(win, "message", listener);
+ callback();
+ }
+ });
+ win.postMessage(messageName, "*");
+ };
+}
+else {
+ exports.nextTick = function(callback, win) {
+ win = win || window;
+ window.setTimeout(callback, 0);
+ };
+}
+
+});
+
+// Most of the following code is taken from SproutCore with a few changes.
+
+ace.define('ace/lib/keys', ['require', 'exports', 'module' , 'ace/lib/oop'], function(require, exports, module) {
+
+
+var oop = require("./oop");
+var Keys = (function() {
+ var ret = {
+ MODIFIER_KEYS: {
+ 16: 'Shift', 17: 'Ctrl', 18: 'Alt', 224: 'Meta'
+ },
+
+ KEY_MODS: {
+ "ctrl": 1, "alt": 2, "option" : 2,
+ "shift": 4, "meta": 8, "command": 8
+ },
+
+ FUNCTION_KEYS : {
+ 8 : "Backspace",
+ 9 : "Tab",
+ 13 : "Return",
+ 19 : "Pause",
+ 27 : "Esc",
+ 32 : "Space",
+ 33 : "PageUp",
+ 34 : "PageDown",
+ 35 : "End",
+ 36 : "Home",
+ 37 : "Left",
+ 38 : "Up",
+ 39 : "Right",
+ 40 : "Down",
+ 44 : "Print",
+ 45 : "Insert",
+ 46 : "Delete",
+ 96 : "Numpad0",
+ 97 : "Numpad1",
+ 98 : "Numpad2",
+ 99 : "Numpad3",
+ 100: "Numpad4",
+ 101: "Numpad5",
+ 102: "Numpad6",
+ 103: "Numpad7",
+ 104: "Numpad8",
+ 105: "Numpad9",
+ 112: "F1",
+ 113: "F2",
+ 114: "F3",
+ 115: "F4",
+ 116: "F5",
+ 117: "F6",
+ 118: "F7",
+ 119: "F8",
+ 120: "F9",
+ 121: "F10",
+ 122: "F11",
+ 123: "F12",
+ 144: "Numlock",
+ 145: "Scrolllock"
+ },
+
+ PRINTABLE_KEYS: {
+ 32: ' ', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5',
+ 54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 61: '=', 65: 'a',
+ 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h',
+ 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o',
+ 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v',
+ 87: 'w', 88: 'x', 89: 'y', 90: 'z', 107: '+', 109: '-', 110: '.',
+ 188: ',', 190: '.', 191: '/', 192: '`', 219: '[', 220: '\\',
+ 221: ']', 222: '\''
+ }
+ };
+
+ // A reverse map of FUNCTION_KEYS
+ for (var i in ret.FUNCTION_KEYS) {
+ var name = ret.FUNCTION_KEYS[i].toLowerCase();
+ ret[name] = parseInt(i, 10);
+ }
+
+ // Add the MODIFIER_KEYS, FUNCTION_KEYS and PRINTABLE_KEYS to the KEY
+ // variables as well.
+ oop.mixin(ret, ret.MODIFIER_KEYS);
+ oop.mixin(ret, ret.PRINTABLE_KEYS);
+ oop.mixin(ret, ret.FUNCTION_KEYS);
+
+ // aliases
+ ret.enter = ret["return"];
+ ret.escape = ret.esc;
+ ret.del = ret["delete"];
+
+ // workaround for firefox bug
+ ret[173] = '-';
+
+ return ret;
+})();
+oop.mixin(exports, Keys);
+
+exports.keyCodeToString = function(keyCode) {
+ return (Keys[keyCode] || String.fromCharCode(keyCode)).toLowerCase();
+}
+
+});
+
+ace.define('ace/lib/oop', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+exports.inherits = (function() {
+ var tempCtor = function() {};
+ return function(ctor, superCtor) {
+ tempCtor.prototype = superCtor.prototype;
+ ctor.super_ = superCtor.prototype;
+ ctor.prototype = new tempCtor();
+ ctor.prototype.constructor = ctor;
+ };
+}());
+
+exports.mixin = function(obj, mixin) {
+ for (var key in mixin) {
+ obj[key] = mixin[key];
+ }
+};
+
+exports.implement = function(proto, mixin) {
+ exports.mixin(proto, mixin);
+};
+
+});
+
+ace.define('ace/lib/useragent', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+var os = (navigator.platform.match(/mac|win|linux/i) || ["other"])[0].toLowerCase();
+var ua = navigator.userAgent;
+
+// Is the user using a browser that identifies itself as Windows
+exports.isWin = (os == "win");
+
+// Is the user using a browser that identifies itself as Mac OS
+exports.isMac = (os == "mac");
+
+// Is the user using a browser that identifies itself as Linux
+exports.isLinux = (os == "linux");
+
+exports.isIE =
+ navigator.appName == "Microsoft Internet Explorer"
+ && parseFloat(navigator.userAgent.match(/MSIE ([0-9]+[\.0-9]+)/)[1]);
+
+exports.isOldIE = exports.isIE && exports.isIE < 9;
+
+// Is this Firefox or related?
+exports.isGecko = exports.isMozilla = window.controllers && window.navigator.product === "Gecko";
+
+// oldGecko == rev < 2.0
+exports.isOldGecko = exports.isGecko && parseInt((navigator.userAgent.match(/rv\:(\d+)/)||[])[1], 10) < 4;
+
+// Is this Opera
+exports.isOpera = window.opera && Object.prototype.toString.call(window.opera) == "[object Opera]";
+
+// Is the user using a browser that identifies itself as WebKit
+exports.isWebKit = parseFloat(ua.split("WebKit/")[1]) || undefined;
+
+exports.isChrome = parseFloat(ua.split(" Chrome/")[1]) || undefined;
+
+exports.isAIR = ua.indexOf("AdobeAIR") >= 0;
+
+exports.isIPad = ua.indexOf("iPad") >= 0;
+
+exports.isTouchPad = ua.indexOf("TouchPad") >= 0;
+exports.OS = {
+ LINUX: "LINUX",
+ MAC: "MAC",
+ WINDOWS: "WINDOWS"
+};
+exports.getOS = function() {
+ if (exports.isMac) {
+ return exports.OS.MAC;
+ } else if (exports.isLinux) {
+ return exports.OS.LINUX;
+ } else {
+ return exports.OS.WINDOWS;
+ }
+};
+
+});
+
+ace.define('ace/editor', ['require', 'exports', 'module' , 'ace/lib/fixoldbrowsers', 'ace/lib/oop', 'ace/lib/lang', 'ace/lib/useragent', 'ace/keyboard/textinput', 'ace/mouse/mouse_handler', 'ace/mouse/fold_handler', 'ace/keyboard/keybinding', 'ace/edit_session', 'ace/search', 'ace/range', 'ace/lib/event_emitter', 'ace/commands/command_manager', 'ace/commands/default_commands'], function(require, exports, module) {
+
+
+require("./lib/fixoldbrowsers");
+
+var oop = require("./lib/oop");
+var lang = require("./lib/lang");
+var useragent = require("./lib/useragent");
+var TextInput = require("./keyboard/textinput").TextInput;
+var MouseHandler = require("./mouse/mouse_handler").MouseHandler;
+var FoldHandler = require("./mouse/fold_handler").FoldHandler;
+var KeyBinding = require("./keyboard/keybinding").KeyBinding;
+var EditSession = require("./edit_session").EditSession;
+var Search = require("./search").Search;
+var Range = require("./range").Range;
+var EventEmitter = require("./lib/event_emitter").EventEmitter;
+var CommandManager = require("./commands/command_manager").CommandManager;
+var defaultCommands = require("./commands/default_commands").commands;
+
+/**
+ * new Editor(renderer, session)
+ * - renderer (VirtualRenderer): Associated `VirtualRenderer` that draws everything
+ * - session (EditSession): The `EditSession` to refer to
+ *
+ * Creates a new `Editor` object.
+ *
+ **/
+var Editor = function(renderer, session) {
+ var container = renderer.getContainerElement();
+ this.container = container;
+ this.renderer = renderer;
+
+ this.commands = new CommandManager(useragent.isMac ? "mac" : "win", defaultCommands);
+ this.textInput = new TextInput(renderer.getTextAreaContainer(), this);
+ this.renderer.textarea = this.textInput.getElement();
+ this.keyBinding = new KeyBinding(this);
+
+ // TODO detect touch event support
+ this.$mouseHandler = new MouseHandler(this);
+ new FoldHandler(this);
+
+ this.$blockScrolling = 0;
+ this.$search = new Search().set({
+ wrap: true
+ });
+
+ this.setSession(session || new EditSession(""));
+};
+
+(function(){
+
+ oop.implement(this, EventEmitter);
+ this.setKeyboardHandler = function(keyboardHandler) {
+ this.keyBinding.setKeyboardHandler(keyboardHandler);
+ };
+ this.getKeyboardHandler = function() {
+ return this.keyBinding.getKeyboardHandler();
+ };
+ /**
+ * Editor@changeSession(e)
+ * - e (Object): An object with two properties, `oldSession` and `session`, that represent the old and new [[EditSession]]s.
+ *
+ * Emitted whenever the [[EditSession]] changes.
+ **/
+ this.setSession = function(session) {
+ if (this.session == session)
+ return;
+
+ if (this.session) {
+ var oldSession = this.session;
+ this.session.removeEventListener("change", this.$onDocumentChange);
+ this.session.removeEventListener("changeMode", this.$onChangeMode);
+ this.session.removeEventListener("tokenizerUpdate", this.$onTokenizerUpdate);
+ this.session.removeEventListener("changeTabSize", this.$onChangeTabSize);
+ this.session.removeEventListener("changeWrapLimit", this.$onChangeWrapLimit);
+ this.session.removeEventListener("changeWrapMode", this.$onChangeWrapMode);
+ this.session.removeEventListener("onChangeFold", this.$onChangeFold);
+ this.session.removeEventListener("changeFrontMarker", this.$onChangeFrontMarker);
+ this.session.removeEventListener("changeBackMarker", this.$onChangeBackMarker);
+ this.session.removeEventListener("changeBreakpoint", this.$onChangeBreakpoint);
+ this.session.removeEventListener("changeAnnotation", this.$onChangeAnnotation);
+ this.session.removeEventListener("changeOverwrite", this.$onCursorChange);
+ this.session.removeEventListener("changeScrollTop", this.$onScrollTopChange);
+ this.session.removeEventListener("changeLeftTop", this.$onScrollLeftChange);
+
+ var selection = this.session.getSelection();
+ selection.removeEventListener("changeCursor", this.$onCursorChange);
+ selection.removeEventListener("changeSelection", this.$onSelectionChange);
+ }
+
+ this.session = session;
+
+ this.$onDocumentChange = this.onDocumentChange.bind(this);
+ session.addEventListener("change", this.$onDocumentChange);
+ this.renderer.setSession(session);
+
+ this.$onChangeMode = this.onChangeMode.bind(this);
+ session.addEventListener("changeMode", this.$onChangeMode);
+
+ this.$onTokenizerUpdate = this.onTokenizerUpdate.bind(this);
+ session.addEventListener("tokenizerUpdate", this.$onTokenizerUpdate);
+
+ this.$onChangeTabSize = this.renderer.onChangeTabSize.bind(this.renderer);
+ session.addEventListener("changeTabSize", this.$onChangeTabSize);
+
+ this.$onChangeWrapLimit = this.onChangeWrapLimit.bind(this);
+ session.addEventListener("changeWrapLimit", this.$onChangeWrapLimit);
+
+ this.$onChangeWrapMode = this.onChangeWrapMode.bind(this);
+ session.addEventListener("changeWrapMode", this.$onChangeWrapMode);
+
+ this.$onChangeFold = this.onChangeFold.bind(this);
+ session.addEventListener("changeFold", this.$onChangeFold);
+
+ this.$onChangeFrontMarker = this.onChangeFrontMarker.bind(this);
+ this.session.addEventListener("changeFrontMarker", this.$onChangeFrontMarker);
+
+ this.$onChangeBackMarker = this.onChangeBackMarker.bind(this);
+ this.session.addEventListener("changeBackMarker", this.$onChangeBackMarker);
+
+ this.$onChangeBreakpoint = this.onChangeBreakpoint.bind(this);
+ this.session.addEventListener("changeBreakpoint", this.$onChangeBreakpoint);
+
+ this.$onChangeAnnotation = this.onChangeAnnotation.bind(this);
+ this.session.addEventListener("changeAnnotation", this.$onChangeAnnotation);
+
+ this.$onCursorChange = this.onCursorChange.bind(this);
+ this.session.addEventListener("changeOverwrite", this.$onCursorChange);
+
+ this.$onScrollTopChange = this.onScrollTopChange.bind(this);
+ this.session.addEventListener("changeScrollTop", this.$onScrollTopChange);
+
+ this.$onScrollLeftChange = this.onScrollLeftChange.bind(this);
+ this.session.addEventListener("changeScrollLeft", this.$onScrollLeftChange);
+
+ this.selection = session.getSelection();
+ this.selection.addEventListener("changeCursor", this.$onCursorChange);
+
+ this.$onSelectionChange = this.onSelectionChange.bind(this);
+ this.selection.addEventListener("changeSelection", this.$onSelectionChange);
+
+ this.onChangeMode();
+
+ this.$blockScrolling += 1;
+ this.onCursorChange();
+ this.$blockScrolling -= 1;
+
+ this.onScrollTopChange();
+ this.onScrollLeftChange();
+ this.onSelectionChange();
+ this.onChangeFrontMarker();
+ this.onChangeBackMarker();
+ this.onChangeBreakpoint();
+ this.onChangeAnnotation();
+ this.session.getUseWrapMode() && this.renderer.adjustWrapLimit();
+ this.renderer.updateFull();
+
+ this._emit("changeSession", {
+ session: session,
+ oldSession: oldSession
+ });
+ };
+ this.getSession = function() {
+ return this.session;
+ };
+ this.setValue = function(val, cursorPos) {
+ this.session.doc.setValue(val);
+
+ if (!cursorPos)
+ this.selectAll();
+ else if (cursorPos == 1)
+ this.navigateFileEnd();
+ else if (cursorPos == -1)
+ this.navigateFileStart();
+
+ return val;
+ };
+ this.getValue = function() {
+ return this.session.getValue();
+ };
+ this.getSelection = function() {
+ return this.selection;
+ };
+ this.resize = function(force) {
+ this.renderer.onResize(force);
+ };
+ this.setTheme = function(theme) {
+ this.renderer.setTheme(theme);
+ };
+ this.getTheme = function() {
+ return this.renderer.getTheme();
+ };
+ this.setStyle = function(style) {
+ this.renderer.setStyle(style);
+ };
+ this.unsetStyle = function(style) {
+ this.renderer.unsetStyle(style);
+ };
+ this.setFontSize = function(size) {
+ this.container.style.fontSize = size;
+ this.renderer.updateFontSize();
+ };
+ this.$highlightBrackets = function() {
+ if (this.session.$bracketHighlight) {
+ this.session.removeMarker(this.session.$bracketHighlight);
+ this.session.$bracketHighlight = null;
+ }
+
+ if (this.$highlightPending) {
+ return;
+ }
+
+ // perform highlight async to not block the browser during navigation
+ var self = this;
+ this.$highlightPending = true;
+ setTimeout(function() {
+ self.$highlightPending = false;
+
+ var pos = self.session.findMatchingBracket(self.getCursorPosition());
+ if (pos) {
+ var range = new Range(pos.row, pos.column, pos.row, pos.column+1);
+ self.session.$bracketHighlight = self.session.addMarker(range, "ace_bracket", "text");
+ }
+ }, 10);
+ };
+ this.focus = function() {
+ // Safari needs the timeout
+ // iOS and Firefox need it called immediately
+ // to be on the save side we do both
+ var _self = this;
+ setTimeout(function() {
+ _self.textInput.focus();
+ });
+ this.textInput.focus();
+ };
+ this.isFocused = function() {
+ return this.textInput.isFocused();
+ };
+ this.blur = function() {
+ this.textInput.blur();
+ };
+ this.onFocus = function() {
+ if (this.$isFocused)
+ return;
+ this.$isFocused = true;
+ this.renderer.showCursor();
+ this.renderer.visualizeFocus();
+ this._emit("focus");
+ };
+ this.onBlur = function() {
+ if (!this.$isFocused)
+ return;
+ this.$isFocused = false;
+ this.renderer.hideCursor();
+ this.renderer.visualizeBlur();
+ this._emit("blur");
+ };
+
+ this.$cursorChange = function() {
+ this.renderer.updateCursor();
+ };
+ this.onDocumentChange = function(e) {
+ var delta = e.data;
+ var range = delta.range;
+ var lastRow;
+
+ if (range.start.row == range.end.row && delta.action != "insertLines" && delta.action != "removeLines")
+ lastRow = range.end.row;
+ else
+ lastRow = Infinity;
+ this.renderer.updateLines(range.start.row, lastRow);
+
+ this._emit("change", e);
+
+ // update cursor because tab characters can influence the cursor position
+ this.$cursorChange();
+ };
+
+ this.onTokenizerUpdate = function(e) {
+ var rows = e.data;
+ this.renderer.updateLines(rows.first, rows.last);
+ };
+
+
+ this.onScrollTopChange = function() {
+ this.renderer.scrollToY(this.session.getScrollTop());
+ };
+
+ this.onScrollLeftChange = function() {
+ this.renderer.scrollToX(this.session.getScrollLeft());
+ };
+ this.onCursorChange = function() {
+ this.$cursorChange();
+
+ if (!this.$blockScrolling) {
+ this.renderer.scrollCursorIntoView();
+ }
+
+ this.$highlightBrackets();
+ this.$updateHighlightActiveLine();
+ this._emit("changeSelection");
+ };
+ this.$updateHighlightActiveLine = function() {
+ var session = this.getSession();
+
+ if (session.$highlightLineMarker)
+ session.removeMarker(session.$highlightLineMarker);
+
+ session.$highlightLineMarker = null;
+
+ if (this.$highlightActiveLine) {
+ var cursor = this.getCursorPosition();
+ var foldLine = this.session.getFoldLine(cursor.row);
+
+ if ((this.getSelectionStyle() != "line" || !this.selection.isMultiLine())) {
+ var range;
+ if (foldLine) {
+ range = new Range(foldLine.start.row, 0, foldLine.end.row + 1, 0);
+ } else {
+ range = new Range(cursor.row, 0, cursor.row+1, 0);
+ }
+ session.$highlightLineMarker = session.addMarker(range, "ace_active_line", "background");
+ }
+ }
+ };
+
+
+ this.onSelectionChange = function(e) {
+ var session = this.session;
+
+ if (session.$selectionMarker) {
+ session.removeMarker(session.$selectionMarker);
+ }
+ session.$selectionMarker = null;
+
+ if (!this.selection.isEmpty()) {
+ var range = this.selection.getRange();
+ var style = this.getSelectionStyle();
+ session.$selectionMarker = session.addMarker(range, "ace_selection", style);
+ } else {
+ this.$updateHighlightActiveLine();
+ }
+
+ var re = this.$highlightSelectedWord && this.$getSelectionHighLightRegexp()
+ this.session.highlight(re);
+
+ this._emit("changeSelection");
+ };
+
+ this.$getSelectionHighLightRegexp = function() {
+ var session = this.session;
+
+ var selection = this.getSelectionRange();
+ if (selection.isEmpty() || selection.isMultiLine())
+ return;
+
+ var startOuter = selection.start.column - 1;
+ var endOuter = selection.end.column + 1;
+ var line = session.getLine(selection.start.row);
+ var lineCols = line.length;
+ var needle = line.substring(Math.max(startOuter, 0),
+ Math.min(endOuter, lineCols));
+
+ // Make sure the outer characters are not part of the word.
+ if ((startOuter >= 0 && /^[\w\d]/.test(needle)) ||
+ (endOuter <= lineCols && /[\w\d]$/.test(needle)))
+ return;
+
+ needle = line.substring(selection.start.column, selection.end.column);
+ if (!/^[\w\d]+$/.test(needle))
+ return;
+
+ var re = this.$search.$assembleRegExp({
+ wholeWord: true,
+ caseSensitive: true,
+ needle: needle
+ });
+
+ return re;
+ };
+
+
+ this.onChangeFrontMarker = function() {
+ this.renderer.updateFrontMarkers();
+ };
+
+ this.onChangeBackMarker = function() {
+ this.renderer.updateBackMarkers();
+ };
+
+
+ this.onChangeBreakpoint = function() {
+ this.renderer.updateBreakpoints();
+ };
+
+ this.onChangeAnnotation = function() {
+ this.renderer.setAnnotations(this.session.getAnnotations());
+ };
+
+
+ this.onChangeMode = function() {
+ this.renderer.updateText();
+ };
+
+
+ this.onChangeWrapLimit = function() {
+ this.renderer.updateFull();
+ };
+
+ this.onChangeWrapMode = function() {
+ this.renderer.onResize(true);
+ };
+
+
+ this.onChangeFold = function() {
+ // Update the active line marker as due to folding changes the current
+ // line range on the screen might have changed.
+ this.$updateHighlightActiveLine();
+ // TODO: This might be too much updating. Okay for now.
+ this.renderer.updateFull();
+ };
+ /**
+ * Editor@copy(text)
+ * - text (String): The copied text
+ *
+ * Emitted when text is copied.
+ **/
+ this.getCopyText = function() {
+ var text = "";
+ if (!this.selection.isEmpty())
+ text = this.session.getTextRange(this.getSelectionRange());
+
+ this._emit("copy", text);
+ return text;
+ };
+ this.onCopy = function() {
+ this.commands.exec("copy", this);
+ };
+ this.onCut = function() {
+ this.commands.exec("cut", this);
+ };
+ /**
+ * Editor@paste(text)
+ * - text (String): The pasted text
+ *
+ * Emitted when text is pasted.
+ **/
+ this.onPaste = function(text) {
+ // todo this should change when paste becomes a command
+ if (this.$readOnly)
+ return;
+ this._emit("paste", text);
+ this.insert(text);
+ };
+ this.insert = function(text) {
+ var session = this.session;
+ var mode = session.getMode();
+
+ var cursor = this.getCursorPosition();
+
+ if (this.getBehavioursEnabled()) {
+ // Get a transform if the current mode wants one.
+ var transform = mode.transformAction(session.getState(cursor.row), 'insertion', this, session, text);
+ if (transform)
+ text = transform.text;
+ }
+
+ text = text.replace("\t", this.session.getTabString());
+
+ // remove selected text
+ if (!this.selection.isEmpty()) {
+ cursor = this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ }
+ else if (this.session.getOverwrite()) {
+ var range = new Range.fromPoints(cursor, cursor);
+ range.end.column += text.length;
+ this.session.remove(range);
+ }
+
+ this.clearSelection();
+
+ var start = cursor.column;
+ var lineState = session.getState(cursor.row);
+ var shouldOutdent = mode.checkOutdent(lineState, session.getLine(cursor.row), text);
+ var line = session.getLine(cursor.row);
+ var lineIndent = mode.getNextLineIndent(lineState, line.slice(0, cursor.column), session.getTabString());
+ var end = session.insert(cursor, text);
+
+ if (transform && transform.selection) {
+ if (transform.selection.length == 2) { // Transform relative to the current column
+ this.selection.setSelectionRange(
+ new Range(cursor.row, start + transform.selection[0],
+ cursor.row, start + transform.selection[1]));
+ } else { // Transform relative to the current row.
+ this.selection.setSelectionRange(
+ new Range(cursor.row + transform.selection[0],
+ transform.selection[1],
+ cursor.row + transform.selection[2],
+ transform.selection[3]));
+ }
+ }
+
+ var lineState = session.getState(cursor.row);
+
+ // TODO disabled multiline auto indent
+ // possibly doing the indent before inserting the text
+ // if (cursor.row !== end.row) {
+ if (session.getDocument().isNewLine(text)) {
+ this.moveCursorTo(cursor.row+1, 0);
+
+ var size = session.getTabSize();
+ var minIndent = Number.MAX_VALUE;
+
+ for (var row = cursor.row + 1; row <= end.row; ++row) {
+ var indent = 0;
+
+ line = session.getLine(row);
+ for (var i = 0; i < line.length; ++i)
+ if (line.charAt(i) == '\t')
+ indent += size;
+ else if (line.charAt(i) == ' ')
+ indent += 1;
+ else
+ break;
+ if (/[^\s]/.test(line))
+ minIndent = Math.min(indent, minIndent);
+ }
+
+ for (var row = cursor.row + 1; row <= end.row; ++row) {
+ var outdent = minIndent;
+
+ line = session.getLine(row);
+ for (var i = 0; i < line.length && outdent > 0; ++i)
+ if (line.charAt(i) == '\t')
+ outdent -= size;
+ else if (line.charAt(i) == ' ')
+ outdent -= 1;
+ session.remove(new Range(row, 0, row, i));
+ }
+ session.indentRows(cursor.row + 1, end.row, lineIndent);
+ }
+ if (shouldOutdent)
+ mode.autoOutdent(lineState, session, cursor.row);
+ };
+
+ this.onTextInput = function(text) {
+ this.keyBinding.onTextInput(text);
+ };
+
+ this.onCommandKey = function(e, hashId, keyCode) {
+ this.keyBinding.onCommandKey(e, hashId, keyCode);
+ };
+ this.setOverwrite = function(overwrite) {
+ this.session.setOverwrite(overwrite);
+ };
+ this.getOverwrite = function() {
+ return this.session.getOverwrite();
+ };
+ this.toggleOverwrite = function() {
+ this.session.toggleOverwrite();
+ };
+ this.setScrollSpeed = function(speed) {
+ this.$mouseHandler.setScrollSpeed(speed);
+ };
+ this.getScrollSpeed = function() {
+ return this.$mouseHandler.getScrollSpeed();
+ };
+ this.setDragDelay = function(dragDelay) {
+ this.$mouseHandler.setDragDelay(dragDelay);
+ };
+ this.getDragDelay = function() {
+ return this.$mouseHandler.getDragDelay();
+ };
+
+ this.$selectionStyle = "line";
+ /**
+ * Editor@changeSelectionStyle(data)
+ * - data (Object): Contains one property, `data`, which indicates the new selection style
+ *
+ * Emitted when the selection style changes, via [[Editor.setSelectionStyle]].
+ *
+ **/
+ this.setSelectionStyle = function(style) {
+ if (this.$selectionStyle == style) return;
+
+ this.$selectionStyle = style;
+ this.onSelectionChange();
+ this._emit("changeSelectionStyle", {data: style});
+ };
+ this.getSelectionStyle = function() {
+ return this.$selectionStyle;
+ };
+
+ this.$highlightActiveLine = true;
+ this.setHighlightActiveLine = function(shouldHighlight) {
+ if (this.$highlightActiveLine == shouldHighlight)
+ return;
+
+ this.$highlightActiveLine = shouldHighlight;
+ this.$updateHighlightActiveLine();
+ };
+ this.getHighlightActiveLine = function() {
+ return this.$highlightActiveLine;
+ };
+
+ this.$highlightGutterLine = true;
+ this.setHighlightGutterLine = function(shouldHighlight) {
+ if (this.$highlightGutterLine == shouldHighlight)
+ return;
+
+ this.renderer.setHighlightGutterLine(shouldHighlight);
+ this.$highlightGutterLine = shouldHighlight;
+ };
+
+ this.getHighlightGutterLine = function() {
+ return this.$highlightGutterLine;
+ };
+
+ this.$highlightSelectedWord = true;
+ this.setHighlightSelectedWord = function(shouldHighlight) {
+ if (this.$highlightSelectedWord == shouldHighlight)
+ return;
+
+ this.$highlightSelectedWord = shouldHighlight;
+ this.$onSelectionChange();
+ };
+ this.getHighlightSelectedWord = function() {
+ return this.$highlightSelectedWord;
+ };
+
+ this.setAnimatedScroll = function(shouldAnimate){
+ this.renderer.setAnimatedScroll(shouldAnimate);
+ };
+
+ this.getAnimatedScroll = function(){
+ return this.renderer.getAnimatedScroll();
+ };
+ this.setShowInvisibles = function(showInvisibles) {
+ this.renderer.setShowInvisibles(showInvisibles);
+ };
+ this.getShowInvisibles = function() {
+ return this.renderer.getShowInvisibles();
+ };
+
+ this.setDisplayIndentGuides = function(display) {
+ this.renderer.setDisplayIndentGuides(display);
+ };
+
+ this.getDisplayIndentGuides = function() {
+ return this.renderer.getDisplayIndentGuides();
+ };
+ this.setShowPrintMargin = function(showPrintMargin) {
+ this.renderer.setShowPrintMargin(showPrintMargin);
+ };
+ this.getShowPrintMargin = function() {
+ return this.renderer.getShowPrintMargin();
+ };
+ this.setPrintMarginColumn = function(showPrintMargin) {
+ this.renderer.setPrintMarginColumn(showPrintMargin);
+ };
+ this.getPrintMarginColumn = function() {
+ return this.renderer.getPrintMarginColumn();
+ };
+
+ this.$readOnly = false;
+ this.setReadOnly = function(readOnly) {
+ this.$readOnly = readOnly;
+ };
+ this.getReadOnly = function() {
+ return this.$readOnly;
+ };
+
+ this.$modeBehaviours = true;
+ this.setBehavioursEnabled = function (enabled) {
+ this.$modeBehaviours = enabled;
+ };
+ this.getBehavioursEnabled = function () {
+ return this.$modeBehaviours;
+ };
+ this.setShowFoldWidgets = function(show) {
+ var gutter = this.renderer.$gutterLayer;
+ if (gutter.getShowFoldWidgets() == show)
+ return;
+
+ this.renderer.$gutterLayer.setShowFoldWidgets(show);
+ this.$showFoldWidgets = show;
+ this.renderer.updateFull();
+ };
+ this.getShowFoldWidgets = function() {
+ return this.renderer.$gutterLayer.getShowFoldWidgets();
+ };
+
+ this.setFadeFoldWidgets = function(show) {
+ this.renderer.setFadeFoldWidgets(show);
+ };
+
+ this.getFadeFoldWidgets = function() {
+ return this.renderer.getFadeFoldWidgets();
+ };
+ this.remove = function(dir) {
+ if (this.selection.isEmpty()){
+ if (dir == "left")
+ this.selection.selectLeft();
+ else
+ this.selection.selectRight();
+ }
+
+ var range = this.getSelectionRange();
+ if (this.getBehavioursEnabled()) {
+ var session = this.session;
+ var state = session.getState(range.start.row);
+ var new_range = session.getMode().transformAction(state, 'deletion', this, session, range);
+ if (new_range)
+ range = new_range;
+ }
+
+ this.session.remove(range);
+ this.clearSelection();
+ };
+ this.removeWordRight = function() {
+ if (this.selection.isEmpty())
+ this.selection.selectWordRight();
+
+ this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ };
+ this.removeWordLeft = function() {
+ if (this.selection.isEmpty())
+ this.selection.selectWordLeft();
+
+ this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ };
+ this.removeToLineStart = function() {
+ if (this.selection.isEmpty())
+ this.selection.selectLineStart();
+
+ this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ };
+ this.removeToLineEnd = function() {
+ if (this.selection.isEmpty())
+ this.selection.selectLineEnd();
+
+ var range = this.getSelectionRange();
+ if (range.start.column == range.end.column && range.start.row == range.end.row) {
+ range.end.column = 0;
+ range.end.row++;
+ }
+
+ this.session.remove(range);
+ this.clearSelection();
+ };
+ this.splitLine = function() {
+ if (!this.selection.isEmpty()) {
+ this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ }
+
+ var cursor = this.getCursorPosition();
+ this.insert("\n");
+ this.moveCursorToPosition(cursor);
+ };
+ this.transposeLetters = function() {
+ if (!this.selection.isEmpty()) {
+ return;
+ }
+
+ var cursor = this.getCursorPosition();
+ var column = cursor.column;
+ if (column === 0)
+ return;
+
+ var line = this.session.getLine(cursor.row);
+ var swap, range;
+ if (column < line.length) {
+ swap = line.charAt(column) + line.charAt(column-1);
+ range = new Range(cursor.row, column-1, cursor.row, column+1);
+ }
+ else {
+ swap = line.charAt(column-1) + line.charAt(column-2);
+ range = new Range(cursor.row, column-2, cursor.row, column);
+ }
+ this.session.replace(range, swap);
+ };
+ this.toLowerCase = function() {
+ var originalRange = this.getSelectionRange();
+ if (this.selection.isEmpty()) {
+ this.selection.selectWord();
+ }
+
+ var range = this.getSelectionRange();
+ var text = this.session.getTextRange(range);
+ this.session.replace(range, text.toLowerCase());
+ this.selection.setSelectionRange(originalRange);
+ };
+ this.toUpperCase = function() {
+ var originalRange = this.getSelectionRange();
+ if (this.selection.isEmpty()) {
+ this.selection.selectWord();
+ }
+
+ var range = this.getSelectionRange();
+ var text = this.session.getTextRange(range);
+ this.session.replace(range, text.toUpperCase());
+ this.selection.setSelectionRange(originalRange);
+ };
+ this.indent = function() {
+ var session = this.session;
+ var range = this.getSelectionRange();
+
+ if (range.start.row < range.end.row || range.start.column < range.end.column) {
+ var rows = this.$getSelectedRows();
+ session.indentRows(rows.first, rows.last, "\t");
+ } else {
+ var indentString;
+
+ if (this.session.getUseSoftTabs()) {
+ var size = session.getTabSize(),
+ position = this.getCursorPosition(),
+ column = session.documentToScreenColumn(position.row, position.column),
+ count = (size - column % size);
+
+ indentString = lang.stringRepeat(" ", count);
+ } else
+ indentString = "\t";
+ return this.insert(indentString);
+ }
+ };
+ this.blockOutdent = function() {
+ var selection = this.session.getSelection();
+ this.session.outdentRows(selection.getRange());
+ };
+ this.toggleCommentLines = function() {
+ var state = this.session.getState(this.getCursorPosition().row);
+ var rows = this.$getSelectedRows();
+ this.session.getMode().toggleCommentLines(state, this.session, rows.first, rows.last);
+ };
+ this.removeLines = function() {
+ var rows = this.$getSelectedRows();
+ var range;
+ if (rows.first === 0 || rows.last+1 < this.session.getLength())
+ range = new Range(rows.first, 0, rows.last+1, 0);
+ else
+ range = new Range(
+ rows.first-1, this.session.getLine(rows.first-1).length,
+ rows.last, this.session.getLine(rows.last).length
+ );
+ this.session.remove(range);
+ this.clearSelection();
+ };
+
+ this.duplicateSelection = function() {
+ var sel = this.selection;
+ var doc = this.session;
+ var range = sel.getRange();
+ if (range.isEmpty()) {
+ var row = range.start.row;
+ doc.duplicateLines(row, row);
+ } else {
+ var reverse = sel.isBackwards()
+ var point = sel.isBackwards() ? range.start : range.end;
+ var endPoint = doc.insert(point, doc.getTextRange(range), false);
+ range.start = point;
+ range.end = endPoint;
+
+ sel.setSelectionRange(range, reverse)
+ }
+ };
+ this.moveLinesDown = function() {
+ this.$moveLines(function(firstRow, lastRow) {
+ return this.session.moveLinesDown(firstRow, lastRow);
+ });
+ };
+ this.moveLinesUp = function() {
+ this.$moveLines(function(firstRow, lastRow) {
+ return this.session.moveLinesUp(firstRow, lastRow);
+ });
+ };
+ this.moveText = function(range, toPosition) {
+ if (this.$readOnly)
+ return null;
+
+ return this.session.moveText(range, toPosition);
+ };
+ this.copyLinesUp = function() {
+ this.$moveLines(function(firstRow, lastRow) {
+ this.session.duplicateLines(firstRow, lastRow);
+ return 0;
+ });
+ };
+ this.copyLinesDown = function() {
+ this.$moveLines(function(firstRow, lastRow) {
+ return this.session.duplicateLines(firstRow, lastRow);
+ });
+ };
+ this.$moveLines = function(mover) {
+ var rows = this.$getSelectedRows();
+ var selection = this.selection;
+ if (!selection.isMultiLine()) {
+ var range = selection.getRange();
+ var reverse = selection.isBackwards();
+ }
+
+ var linesMoved = mover.call(this, rows.first, rows.last);
+
+ if (range) {
+ range.start.row += linesMoved;
+ range.end.row += linesMoved;
+ selection.setSelectionRange(range, reverse);
+ }
+ else {
+ selection.setSelectionAnchor(rows.last+linesMoved+1, 0);
+ selection.$moveSelection(function() {
+ selection.moveCursorTo(rows.first+linesMoved, 0);
+ });
+ }
+ };
+ this.$getSelectedRows = function() {
+ var range = this.getSelectionRange().collapseRows();
+
+ return {
+ first: range.start.row,
+ last: range.end.row
+ };
+ };
+
+ this.onCompositionStart = function(text) {
+ this.renderer.showComposition(this.getCursorPosition());
+ };
+
+ this.onCompositionUpdate = function(text) {
+ this.renderer.setCompositionText(text);
+ };
+
+ this.onCompositionEnd = function() {
+ this.renderer.hideComposition();
+ };
+ this.getFirstVisibleRow = function() {
+ return this.renderer.getFirstVisibleRow();
+ };
+ this.getLastVisibleRow = function() {
+ return this.renderer.getLastVisibleRow();
+ };
+ this.isRowVisible = function(row) {
+ return (row >= this.getFirstVisibleRow() && row <= this.getLastVisibleRow());
+ };
+ this.isRowFullyVisible = function(row) {
+ return (row >= this.renderer.getFirstFullyVisibleRow() && row <= this.renderer.getLastFullyVisibleRow());
+ };
+ this.$getVisibleRowCount = function() {
+ return this.renderer.getScrollBottomRow() - this.renderer.getScrollTopRow() + 1;
+ };
+
+ this.$moveByPage = function(dir, select) {
+ var renderer = this.renderer;
+ var config = this.renderer.layerConfig;
+ var rows = dir * Math.floor(config.height / config.lineHeight);
+
+ this.$blockScrolling++;
+ if (select == true) {
+ this.selection.$moveSelection(function(){
+ this.moveCursorBy(rows, 0);
+ });
+ } else if (select == false) {
+ this.selection.moveCursorBy(rows, 0);
+ this.selection.clearSelection();
+ }
+ this.$blockScrolling--;
+
+ var scrollTop = renderer.scrollTop;
+
+ renderer.scrollBy(0, rows * config.lineHeight);
+ if (select != null)
+ renderer.scrollCursorIntoView(null, 0.5);
+
+ renderer.animateScrolling(scrollTop);
+ };
+ this.selectPageDown = function() {
+ this.$moveByPage(1, true);
+ };
+ this.selectPageUp = function() {
+ this.$moveByPage(-1, true);
+ };
+ this.gotoPageDown = function() {
+ this.$moveByPage(1, false);
+ };
+ this.gotoPageUp = function() {
+ this.$moveByPage(-1, false);
+ };
+ this.scrollPageDown = function() {
+ this.$moveByPage(1);
+ };
+ this.scrollPageUp = function() {
+ this.$moveByPage(-1);
+ };
+ this.scrollToRow = function(row) {
+ this.renderer.scrollToRow(row);
+ };
+ this.scrollToLine = function(line, center, animate, callback) {
+ this.renderer.scrollToLine(line, center, animate, callback);
+ };
+ this.centerSelection = function() {
+ var range = this.getSelectionRange();
+ var pos = {
+ row: Math.floor(range.start.row + (range.end.row - range.start.row) / 2),
+ column: Math.floor(range.start.column + (range.end.column - range.start.column) / 2)
+ }
+ this.renderer.alignCursor(pos, 0.5);
+ };
+ this.getCursorPosition = function() {
+ return this.selection.getCursor();
+ };
+ this.getCursorPositionScreen = function() {
+ return this.session.documentToScreenPosition(this.getCursorPosition());
+ };
+ this.getSelectionRange = function() {
+ return this.selection.getRange();
+ };
+ this.selectAll = function() {
+ this.$blockScrolling += 1;
+ this.selection.selectAll();
+ this.$blockScrolling -= 1;
+ };
+ this.clearSelection = function() {
+ this.selection.clearSelection();
+ };
+ this.moveCursorTo = function(row, column) {
+ this.selection.moveCursorTo(row, column);
+ };
+ this.moveCursorToPosition = function(pos) {
+ this.selection.moveCursorToPosition(pos);
+ };
+ this.jumpToMatching = function(select) {
+ var cursor = this.getCursorPosition();
+
+ var range = this.session.getBracketRange(cursor);
+ if (!range) {
+ range = this.find({
+ needle: /[{}()\[\]]/g,
+ preventScroll:true,
+ start: {row: cursor.row, column: cursor.column - 1}
+ });
+ if (!range)
+ return;
+ var pos = range.start;
+ if (pos.row == cursor.row && Math.abs(pos.column - cursor.column) < 2)
+ range = this.session.getBracketRange(pos);
+ }
+
+ pos = range && range.cursor || pos;
+ if (pos) {
+ if (select) {
+ if (range && range.isEqual(this.getSelectionRange()))
+ this.clearSelection();
+ else
+ this.selection.selectTo(pos.row, pos.column);
+ } else {
+ this.clearSelection();
+ this.moveCursorTo(pos.row, pos.column);
+ }
+ }
+ };
+ this.gotoLine = function(lineNumber, column, animate) {
+ this.selection.clearSelection();
+ this.session.unfold({row: lineNumber - 1, column: column || 0});
+
+ this.$blockScrolling += 1;
+ this.moveCursorTo(lineNumber - 1, column || 0);
+ this.$blockScrolling -= 1;
+
+ if (!this.isRowFullyVisible(lineNumber - 1))
+ this.scrollToLine(lineNumber - 1, true, animate);
+ };
+ this.navigateTo = function(row, column) {
+ this.clearSelection();
+ this.moveCursorTo(row, column);
+ };
+ this.navigateUp = function(times) {
+ this.selection.clearSelection();
+ times = times || 1;
+ this.selection.moveCursorBy(-times, 0);
+ };
+ this.navigateDown = function(times) {
+ this.selection.clearSelection();
+ times = times || 1;
+ this.selection.moveCursorBy(times, 0);
+ };
+ this.navigateLeft = function(times) {
+ if (!this.selection.isEmpty()) {
+ var selectionStart = this.getSelectionRange().start;
+ this.moveCursorToPosition(selectionStart);
+ }
+ else {
+ times = times || 1;
+ while (times--) {
+ this.selection.moveCursorLeft();
+ }
+ }
+ this.clearSelection();
+ };
+ this.navigateRight = function(times) {
+ if (!this.selection.isEmpty()) {
+ var selectionEnd = this.getSelectionRange().end;
+ this.moveCursorToPosition(selectionEnd);
+ }
+ else {
+ times = times || 1;
+ while (times--) {
+ this.selection.moveCursorRight();
+ }
+ }
+ this.clearSelection();
+ };
+ this.navigateLineStart = function() {
+ this.selection.moveCursorLineStart();
+ this.clearSelection();
+ };
+ this.navigateLineEnd = function() {
+ this.selection.moveCursorLineEnd();
+ this.clearSelection();
+ };
+ this.navigateFileEnd = function() {
+ var scrollTop = this.renderer.scrollTop;
+ this.selection.moveCursorFileEnd();
+ this.clearSelection();
+ this.renderer.animateScrolling(scrollTop);
+ };
+ this.navigateFileStart = function() {
+ var scrollTop = this.renderer.scrollTop;
+ this.selection.moveCursorFileStart();
+ this.clearSelection();
+ this.renderer.animateScrolling(scrollTop);
+ };
+ this.navigateWordRight = function() {
+ this.selection.moveCursorWordRight();
+ this.clearSelection();
+ };
+ this.navigateWordLeft = function() {
+ this.selection.moveCursorWordLeft();
+ this.clearSelection();
+ };
+ this.replace = function(replacement, options) {
+ if (options)
+ this.$search.set(options);
+
+ var range = this.$search.find(this.session);
+ var replaced = 0;
+ if (!range)
+ return replaced;
+
+ if (this.$tryReplace(range, replacement)) {
+ replaced = 1;
+ }
+ if (range !== null) {
+ this.selection.setSelectionRange(range);
+ this.renderer.scrollSelectionIntoView(range.start, range.end);
+ }
+
+ return replaced;
+ };
+ this.replaceAll = function(replacement, options) {
+ if (options) {
+ this.$search.set(options);
+ }
+
+ var ranges = this.$search.findAll(this.session);
+ var replaced = 0;
+ if (!ranges.length)
+ return replaced;
+
+ this.$blockScrolling += 1;
+
+ var selection = this.getSelectionRange();
+ this.clearSelection();
+ this.selection.moveCursorTo(0, 0);
+
+ for (var i = ranges.length - 1; i >= 0; --i) {
+ if(this.$tryReplace(ranges[i], replacement)) {
+ replaced++;
+ }
+ }
+
+ this.selection.setSelectionRange(selection);
+ this.$blockScrolling -= 1;
+
+ return replaced;
+ };
+
+ this.$tryReplace = function(range, replacement) {
+ var input = this.session.getTextRange(range);
+ replacement = this.$search.replace(input, replacement);
+ if (replacement !== null) {
+ range.end = this.session.replace(range, replacement);
+ return range;
+ } else {
+ return null;
+ }
+ };
+ this.getLastSearchOptions = function() {
+ return this.$search.getOptions();
+ };
+ this.find = function(needle, options, animate) {
+ if (!options)
+ options = {};
+
+ if (typeof needle == "string" || needle instanceof RegExp)
+ options.needle = needle;
+ else if (typeof needle == "object")
+ oop.mixin(options, needle);
+
+ var range = this.selection.getRange();
+ if (options.needle == null) {
+ needle = this.session.getTextRange(range)
+ || this.$search.$options.needle;
+ if (!needle) {
+ range = this.session.getWordRange(range.start.row, range.start.column);
+ needle = this.session.getTextRange(range);
+ }
+ this.$search.set({needle: needle});
+ }
+
+ this.$search.set(options);
+ if (!options.start)
+ this.$search.set({start: range});
+
+ var newRange = this.$search.find(this.session);
+ if (options.preventScroll)
+ return newRange;
+ if (newRange) {
+ this.revealRange(newRange, animate);
+ return newRange;
+ }
+ // clear selection if nothing is found
+ if (options.backwards)
+ range.start = range.end;
+ else
+ range.end = range.start;
+ this.selection.setRange(range);
+ };
+ this.findNext = function(options, animate) {
+ this.find({skipCurrent: true, backwards: false}, options, animate);
+ };
+ this.findPrevious = function(options, animate) {
+ this.find(options, {skipCurrent: true, backwards: true}, animate);
+ };
+
+ this.revealRange = function(range, animate) {
+ this.$blockScrolling += 1;
+ this.session.unfold(range);
+ this.selection.setSelectionRange(range);
+ this.$blockScrolling -= 1;
+
+ var scrollTop = this.renderer.scrollTop;
+ this.renderer.scrollSelectionIntoView(range.start, range.end, 0.5);
+ if (animate != false)
+ this.renderer.animateScrolling(scrollTop);
+ };
+ this.undo = function() {
+ this.$blockScrolling++;
+ this.session.getUndoManager().undo();
+ this.$blockScrolling--;
+ this.renderer.scrollCursorIntoView(null, 0.5);
+ };
+ this.redo = function() {
+ this.$blockScrolling++;
+ this.session.getUndoManager().redo();
+ this.$blockScrolling--;
+ this.renderer.scrollCursorIntoView(null, 0.5);
+ };
+ this.destroy = function() {
+ this.renderer.destroy();
+ };
+
+}).call(Editor.prototype);
+
+
+exports.Editor = Editor;
+});
+
+ace.define('ace/lib/lang', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+exports.stringReverse = function(string) {
+ return string.split("").reverse().join("");
+};
+
+exports.stringRepeat = function (string, count) {
+ return new Array(count + 1).join(string);
+};
+
+var trimBeginRegexp = /^\s\s*/;
+var trimEndRegexp = /\s\s*$/;
+
+exports.stringTrimLeft = function (string) {
+ return string.replace(trimBeginRegexp, '');
+};
+
+exports.stringTrimRight = function (string) {
+ return string.replace(trimEndRegexp, '');
+};
+
+exports.copyObject = function(obj) {
+ var copy = {};
+ for (var key in obj) {
+ copy[key] = obj[key];
+ }
+ return copy;
+};
+
+exports.copyArray = function(array){
+ var copy = [];
+ for (var i=0, l=array.length; i 1) {
+ if (value.charAt(0) == PLACEHOLDER)
+ value = value.substr(1);
+ else if (value.charAt(value.length - 1) == PLACEHOLDER)
+ value = value.slice(0, -1);
+ }
+
+ if (value && value != PLACEHOLDER) {
+ if (pasted)
+ host.onPaste(value);
+ else
+ host.onTextInput(value);
+ }
+ }
+ }
+
+ copied = false;
+ pasted = false;
+
+ // Safari doesn't fire copy events if no text is selected
+ reset(true);
+ }
+
+ var onTextInput = function(e) {
+ if (!inCompostion)
+ sendText(e.data);
+ setTimeout(function () {
+ if (!inCompostion)
+ reset(true);
+ }, 0);
+ };
+
+ var onPropertyChange = function(e) {
+ setTimeout(function() {
+ if (!inCompostion)
+ if(text.value != "") {
+ sendText();
+ }
+ }, 0);
+ };
+
+ var onCompositionStart = function(e) {
+ inCompostion = true;
+ host.onCompositionStart();
+ setTimeout(onCompositionUpdate, 0);
+ };
+
+ var onCompositionUpdate = function() {
+ if (!inCompostion) return;
+ host.onCompositionUpdate(text.value);
+ };
+
+ var onCompositionEnd = function(e) {
+ inCompostion = false;
+ host.onCompositionEnd();
+ };
+
+ var onCopy = function(e) {
+ copied = true;
+ var copyText = host.getCopyText();
+ if(copyText)
+ text.value = copyText;
+ else
+ e.preventDefault();
+ reset();
+ setTimeout(function () {
+ sendText();
+ }, 0);
+ };
+
+ var onCut = function(e) {
+ copied = true;
+ var copyText = host.getCopyText();
+ if(copyText) {
+ text.value = copyText;
+ host.onCut();
+ } else
+ e.preventDefault();
+ reset();
+ setTimeout(function () {
+ sendText();
+ }, 0);
+ };
+
+ event.addCommandKeyListener(text, host.onCommandKey.bind(host));
+ event.addListener(text, "input", onTextInput);
+
+ if (useragent.isOldIE) {
+ var keytable = { 13:1, 27:1 };
+ event.addListener(text, "keyup", function (e) {
+ if (inCompostion && (!text.value || keytable[e.keyCode]))
+ setTimeout(onCompositionEnd, 0);
+ if ((text.value.charCodeAt(0)|0) < 129) {
+ return;
+ }
+ inCompostion ? onCompositionUpdate() : onCompositionStart();
+ });
+
+ event.addListener(text, "propertychange", function() {
+ if (text.value != PLACEHOLDER)
+ setTimeout(sendText, 0);
+ });
+ }
+
+ event.addListener(text, "paste", function(e) {
+ // Mark that the next input text comes from past.
+ pasted = true;
+ // Some browsers support the event.clipboardData API. Use this to get
+ // the pasted content which increases speed if pasting a lot of lines.
+ if (e.clipboardData && e.clipboardData.getData) {
+ sendText(e.clipboardData.getData("text/plain"));
+ e.preventDefault();
+ }
+ else {
+ // If a browser doesn't support any of the things above, use the regular
+ // method to detect the pasted input.
+ onPropertyChange();
+ }
+ });
+
+ if ("onbeforecopy" in text && typeof clipboardData !== "undefined") {
+ event.addListener(text, "beforecopy", function(e) {
+ if (tempStyle)
+ return; // without this text is copied when contextmenu is shown
+ var copyText = host.getCopyText();
+ if (copyText)
+ clipboardData.setData("Text", copyText);
+ else
+ e.preventDefault();
+ });
+ event.addListener(parentNode, "keydown", function(e) {
+ if (e.ctrlKey && e.keyCode == 88) {
+ var copyText = host.getCopyText();
+ if (copyText) {
+ clipboardData.setData("Text", copyText);
+ host.onCut();
+ }
+ event.preventDefault(e);
+ }
+ });
+ event.addListener(text, "cut", onCut); // for ie9 context menu
+ }
+ else if (useragent.isOpera && !("KeyboardEvent" in window)) {
+ event.addListener(parentNode, "keydown", function(e) {
+ if ((useragent.isMac && !e.metaKey) || !e.ctrlKey)
+ return;
+
+ if ((e.keyCode == 88 || e.keyCode == 67)) {
+ var copyText = host.getCopyText();
+ if (copyText) {
+ text.value = copyText;
+ text.select();
+ if (e.keyCode == 88)
+ host.onCut();
+ }
+ }
+ });
+ }
+ else {
+ event.addListener(text, "copy", onCopy);
+ event.addListener(text, "cut", onCut);
+ }
+
+ event.addListener(text, "compositionstart", onCompositionStart);
+ if (useragent.isGecko) {
+ event.addListener(text, "text", onCompositionUpdate);
+ }
+ if (useragent.isWebKit) {
+ event.addListener(text, "keyup", onCompositionUpdate);
+ }
+ event.addListener(text, "compositionend", onCompositionEnd);
+
+ event.addListener(text, "blur", function() {
+ host.onBlur();
+ });
+
+ event.addListener(text, "focus", function() {
+ host.onFocus();
+ reset();
+ });
+
+ this.focus = function() {
+ reset();
+ text.focus();
+ };
+
+ this.blur = function() {
+ text.blur();
+ };
+
+ function isFocused() {
+ return document.activeElement === text;
+ }
+ this.isFocused = isFocused;
+
+ this.getElement = function() {
+ return text;
+ };
+
+ this.onContextMenu = function(e) {
+ if (!tempStyle)
+ tempStyle = text.style.cssText;
+
+ text.style.cssText =
+ "position:fixed; z-index:100000;" +
+ (useragent.isIE ? "background:rgba(0, 0, 0, 0.03); opacity:0.1;" : "") + //"background:rgba(250, 0, 0, 0.3); opacity:1;" +
+ "left:" + (e.clientX - 2) + "px; top:" + (e.clientY - 2) + "px;";
+
+ if (host.selection.isEmpty())
+ text.value = "";
+ else
+ reset(true);
+
+ if (e.type != "mousedown")
+ return;
+
+ if (host.renderer.$keepTextAreaAtCursor)
+ host.renderer.$keepTextAreaAtCursor = null;
+
+ // on windows context menu is opened after mouseup
+ if (useragent.isWin && (useragent.isGecko || useragent.isIE))
+ event.capture(host.container, function(e) {
+ text.style.left = e.clientX - 2 + "px";
+ text.style.top = e.clientY - 2 + "px";
+ }, onContextMenuClose);
+ };
+
+ function onContextMenuClose() {
+ setTimeout(function () {
+ if (tempStyle) {
+ text.style.cssText = tempStyle;
+ tempStyle = '';
+ }
+ sendText();
+ if (host.renderer.$keepTextAreaAtCursor == null) {
+ host.renderer.$keepTextAreaAtCursor = true;
+ host.renderer.$moveTextAreaToCursor();
+ }
+ }, 0);
+ };
+ this.onContextMenuClose = onContextMenuClose;
+
+ // firefox fires contextmenu event after opening it
+ if (!useragent.isGecko)
+ event.addListener(text, "contextmenu", function(e) {
+ host.textInput.onContextMenu(e);
+ onContextMenuClose()
+ });
+};
+
+exports.TextInput = TextInput;
+});
+
+ace.define('ace/mouse/mouse_handler', ['require', 'exports', 'module' , 'ace/lib/event', 'ace/lib/useragent', 'ace/mouse/default_handlers', 'ace/mouse/default_gutter_handler', 'ace/mouse/mouse_event', 'ace/mouse/dragdrop'], function(require, exports, module) {
+
+
+var event = require("../lib/event");
+var useragent = require("../lib/useragent");
+var DefaultHandlers = require("./default_handlers").DefaultHandlers;
+var DefaultGutterHandler = require("./default_gutter_handler").GutterHandler;
+var MouseEvent = require("./mouse_event").MouseEvent;
+var DragdropHandler = require("./dragdrop").DragdropHandler;
+
+var MouseHandler = function(editor) {
+ this.editor = editor;
+
+ new DefaultHandlers(this);
+ new DefaultGutterHandler(this);
+ new DragdropHandler(this);
+
+ event.addListener(editor.container, "mousedown", function(e) {
+ editor.focus();
+ return event.preventDefault(e);
+ });
+
+ var mouseTarget = editor.renderer.getMouseEventTarget();
+ event.addListener(mouseTarget, "click", this.onMouseEvent.bind(this, "click"));
+ event.addListener(mouseTarget, "mousemove", this.onMouseMove.bind(this, "mousemove"));
+ event.addMultiMouseDownListener(mouseTarget, [300, 300, 250], this, "onMouseEvent");
+ event.addMouseWheelListener(editor.container, this.onMouseWheel.bind(this, "mousewheel"));
+
+ var gutterEl = editor.renderer.$gutter;
+ event.addListener(gutterEl, "mousedown", this.onMouseEvent.bind(this, "guttermousedown"));
+ event.addListener(gutterEl, "click", this.onMouseEvent.bind(this, "gutterclick"));
+ event.addListener(gutterEl, "dblclick", this.onMouseEvent.bind(this, "gutterdblclick"));
+ event.addListener(gutterEl, "mousemove", this.onMouseEvent.bind(this, "guttermousemove"));
+};
+
+(function() {
+
+ this.$scrollSpeed = 1;
+ this.setScrollSpeed = function(speed) {
+ this.$scrollSpeed = speed;
+ };
+
+ this.getScrollSpeed = function() {
+ return this.$scrollSpeed;
+ };
+
+ this.onMouseEvent = function(name, e) {
+ this.editor._emit(name, new MouseEvent(e, this.editor));
+ };
+
+ this.$dragDelay = 250;
+ this.setDragDelay = function(dragDelay) {
+ this.$dragDelay = dragDelay;
+ };
+
+ this.getDragDelay = function() {
+ return this.$dragDelay;
+ };
+
+ this.onMouseMove = function(name, e) {
+ // optimization, because mousemove doesn't have a default handler.
+ var listeners = this.editor._eventRegistry && this.editor._eventRegistry.mousemove;
+ if (!listeners || !listeners.length)
+ return;
+
+ this.editor._emit(name, new MouseEvent(e, this.editor));
+ };
+
+ this.onMouseWheel = function(name, e) {
+ var mouseEvent = new MouseEvent(e, this.editor);
+ mouseEvent.speed = this.$scrollSpeed * 2;
+ mouseEvent.wheelX = e.wheelX;
+ mouseEvent.wheelY = e.wheelY;
+
+ this.editor._emit(name, mouseEvent);
+ };
+
+ this.setState = function(state) {
+ this.state = state;
+ };
+
+ this.captureMouse = function(ev, state) {
+ if (state)
+ this.setState(state);
+
+ this.x = ev.x;
+ this.y = ev.y;
+
+ // do not move textarea during selection
+ var renderer = this.editor.renderer;
+ if (renderer.$keepTextAreaAtCursor)
+ renderer.$keepTextAreaAtCursor = null;
+
+ var self = this;
+ var onMouseMove = function(e) {
+ self.x = e.clientX;
+ self.y = e.clientY;
+ };
+
+ var onCaptureEnd = function(e) {
+ clearInterval(timerId);
+ self[self.state + "End"] && self[self.state + "End"](e);
+ self.$clickSelection = null;
+ if (renderer.$keepTextAreaAtCursor == null) {
+ renderer.$keepTextAreaAtCursor = true;
+ renderer.$moveTextAreaToCursor();
+ }
+ };
+
+ var onCaptureInterval = function() {
+ self[self.state] && self[self.state]();
+ }
+
+ if (useragent.isOldIE && ev.domEvent.type == "dblclick") {
+ setTimeout(function() {
+ onCaptureInterval();
+ onCaptureEnd(ev.domEvent);
+ });
+ return;
+ }
+
+ event.capture(this.editor.container, onMouseMove, onCaptureEnd);
+ var timerId = setInterval(onCaptureInterval, 20);
+ };
+}).call(MouseHandler.prototype);
+
+exports.MouseHandler = MouseHandler;
+});
+
+ace.define('ace/mouse/default_handlers', ['require', 'exports', 'module' , 'ace/lib/dom', 'ace/lib/useragent'], function(require, exports, module) {
+
+
+var dom = require("../lib/dom");
+var useragent = require("../lib/useragent");
+
+var DRAG_OFFSET = 5; // pixels
+
+function DefaultHandlers(mouseHandler) {
+ mouseHandler.$clickSelection = null;
+
+ var editor = mouseHandler.editor;
+ editor.setDefaultHandler("mousedown", this.onMouseDown.bind(mouseHandler));
+ editor.setDefaultHandler("dblclick", this.onDoubleClick.bind(mouseHandler));
+ editor.setDefaultHandler("tripleclick", this.onTripleClick.bind(mouseHandler));
+ editor.setDefaultHandler("quadclick", this.onQuadClick.bind(mouseHandler));
+ editor.setDefaultHandler("mousewheel", this.onMouseWheel.bind(mouseHandler));
+
+ var exports = ["select", "startSelect", "drag", "dragEnd", "dragWait",
+ "dragWaitEnd", "startDrag", "focusWait"];
+
+ exports.forEach(function(x) {
+ mouseHandler[x] = this[x];
+ }, this);
+
+ mouseHandler.selectByLines = this.extendSelectionBy.bind(mouseHandler, "getLineRange");
+ mouseHandler.selectByWords = this.extendSelectionBy.bind(mouseHandler, "getWordRange");
+
+ mouseHandler.$focusWaitTimout = 250;
+}
+
+(function() {
+
+ this.onMouseDown = function(ev) {
+ var inSelection = ev.inSelection();
+ var pos = ev.getDocumentPosition();
+ this.mousedownEvent = ev;
+ var editor = this.editor;
+
+ var button = ev.getButton();
+ if (button !== 0) {
+ var selectionRange = editor.getSelectionRange();
+ var selectionEmpty = selectionRange.isEmpty();
+
+ if (selectionEmpty) {
+ editor.moveCursorToPosition(pos);
+ editor.selection.clearSelection();
+ }
+
+ // 2: contextmenu, 1: linux paste
+ editor.textInput.onContextMenu(ev.domEvent);
+ return; // stopping event here breaks contextmenu on ff mac
+ }
+
+ // if this click caused the editor to be focused should not clear the
+ // selection
+ if (inSelection && !editor.isFocused()) {
+ editor.focus();
+ if (this.$focusWaitTimout && !this.$clickSelection) {
+ this.setState("focusWait");
+ this.captureMouse(ev);
+ return ev.preventDefault();
+ }
+ }
+
+ if (!inSelection || this.$clickSelection || ev.getShiftKey()) {
+ // Directly pick STATE_SELECT, since the user is not clicking inside
+ // a selection.
+ this.startSelect(pos);
+ } else if (inSelection) {
+ this.mousedownEvent.time = (new Date()).getTime();
+ this.setState("dragWait");
+ }
+
+ this.captureMouse(ev);
+ return ev.preventDefault();
+ };
+
+ this.startSelect = function(pos) {
+ pos = pos || this.editor.renderer.screenToTextCoordinates(this.x, this.y);
+ if (this.mousedownEvent.getShiftKey()) {
+ this.editor.selection.selectToPosition(pos);
+ }
+ else if (!this.$clickSelection) {
+ this.editor.moveCursorToPosition(pos);
+ this.editor.selection.clearSelection();
+ }
+ this.setState("select");
+ };
+
+ this.select = function() {
+ var anchor, editor = this.editor;
+ var cursor = editor.renderer.screenToTextCoordinates(this.x, this.y);
+
+ if (this.$clickSelection) {
+ var cmp = this.$clickSelection.comparePoint(cursor);
+
+ if (cmp == -1) {
+ anchor = this.$clickSelection.end;
+ } else if (cmp == 1) {
+ anchor = this.$clickSelection.start;
+ } else {
+ var orientedRange = calcRangeOrientation(this.$clickSelection, cursor);
+ cursor = orientedRange.cursor;
+ anchor = orientedRange.anchor;
+ }
+ editor.selection.setSelectionAnchor(anchor.row, anchor.column);
+ }
+ editor.selection.selectToPosition(cursor);
+
+ editor.renderer.scrollCursorIntoView();
+ };
+
+ this.extendSelectionBy = function(unitName) {
+ var anchor, editor = this.editor;
+ var cursor = editor.renderer.screenToTextCoordinates(this.x, this.y);
+ var range = editor.selection[unitName](cursor.row, cursor.column);
+
+ if (this.$clickSelection) {
+ var cmpStart = this.$clickSelection.comparePoint(range.start);
+ var cmpEnd = this.$clickSelection.comparePoint(range.end);
+
+ if (cmpStart == -1 && cmpEnd <= 0) {
+ anchor = this.$clickSelection.end;
+ if (range.end.row != cursor.row || range.end.column != cursor.column)
+ cursor = range.start;
+ } else if (cmpEnd == 1 && cmpStart >= 0) {
+ anchor = this.$clickSelection.start;
+ if (range.start.row != cursor.row || range.start.column != cursor.column)
+ cursor = range.end;
+ } else if (cmpStart == -1 && cmpEnd == 1) {
+ cursor = range.end;
+ anchor = range.start;
+ } else {
+ var orientedRange = calcRangeOrientation(this.$clickSelection, cursor);
+ cursor = orientedRange.cursor;
+ anchor = orientedRange.anchor;
+ }
+ editor.selection.setSelectionAnchor(anchor.row, anchor.column);
+ }
+ editor.selection.selectToPosition(cursor);
+
+ editor.renderer.scrollCursorIntoView();
+ };
+
+ this.startDrag = function() {
+ var editor = this.editor;
+ this.setState("drag");
+ this.dragRange = editor.getSelectionRange();
+ var style = editor.getSelectionStyle();
+ this.dragSelectionMarker = editor.session.addMarker(this.dragRange, "ace_selection", style);
+ editor.clearSelection();
+ dom.addCssClass(editor.container, "ace_dragging");
+ if (!this.$dragKeybinding) {
+ this.$dragKeybinding = {
+ handleKeyboard: function(data, hashId, keyString, keyCode) {
+ if (keyString == "esc")
+ return {command: this.command};
+ },
+ command: {
+ exec: function(editor) {
+ var self = editor.$mouseHandler;
+ self.dragCursor = null;
+ self.dragEnd();
+ self.startSelect();
+ }
+ }
+ }
+ }
+
+ editor.keyBinding.addKeyboardHandler(this.$dragKeybinding);
+ };
+
+ this.focusWait = function() {
+ var distance = calcDistance(this.mousedownEvent.x, this.mousedownEvent.y, this.x, this.y);
+ var time = (new Date()).getTime();
+
+ if (distance > DRAG_OFFSET ||time - this.mousedownEvent.time > this.$focusWaitTimout)
+ this.startSelect();
+ };
+
+ this.dragWait = function(e) {
+ var distance = calcDistance(this.mousedownEvent.x, this.mousedownEvent.y, this.x, this.y);
+ var time = (new Date()).getTime();
+ var editor = this.editor;
+
+ if (distance > DRAG_OFFSET) {
+ this.startSelect(this.mousedownEvent.getDocumentPosition());
+ } else if (time - this.mousedownEvent.time > editor.getDragDelay()) {
+ this.startDrag();
+ }
+ };
+
+ this.dragWaitEnd = function(e) {
+ this.mousedownEvent.domEvent = e;
+ this.startSelect();
+ };
+
+ this.drag = function() {
+ var editor = this.editor;
+ this.dragCursor = editor.renderer.screenToTextCoordinates(this.x, this.y);
+ editor.moveCursorToPosition(this.dragCursor);
+ editor.renderer.scrollCursorIntoView();
+ };
+
+ this.dragEnd = function(e) {
+ var editor = this.editor;
+ var dragCursor = this.dragCursor;
+ var dragRange = this.dragRange;
+ dom.removeCssClass(editor.container, "ace_dragging");
+ editor.session.removeMarker(this.dragSelectionMarker);
+ editor.keyBinding.removeKeyboardHandler(this.$dragKeybinding);
+
+ if (!dragCursor)
+ return;
+
+ editor.clearSelection();
+ if (e && (e.ctrlKey || e.altKey)) {
+ var session = editor.session;
+ var newRange = dragRange;
+ newRange.end = session.insert(dragCursor, session.getTextRange(dragRange));
+ newRange.start = dragCursor;
+ } else if (dragRange.contains(dragCursor.row, dragCursor.column)) {
+ return;
+ } else {
+ var newRange = editor.moveText(dragRange, dragCursor);
+ }
+
+ if (!newRange)
+ return;
+
+ editor.selection.setSelectionRange(newRange);
+ };
+
+ this.onDoubleClick = function(ev) {
+ var pos = ev.getDocumentPosition();
+ var editor = this.editor;
+ var session = editor.session;
+
+ var range = session.getBracketRange(pos);
+ if (range) {
+ if (range.isEmpty()) {
+ range.start.column--;
+ range.end.column++;
+ }
+ this.$clickSelection = range;
+ this.setState("select");
+ return;
+ }
+
+ this.$clickSelection = editor.selection.getWordRange(pos.row, pos.column);
+ this.setState("selectByWords");
+ };
+
+ this.onTripleClick = function(ev) {
+ var pos = ev.getDocumentPosition();
+ var editor = this.editor;
+
+ this.setState("selectByLines");
+ this.$clickSelection = editor.selection.getLineRange(pos.row);
+ };
+
+ this.onQuadClick = function(ev) {
+ var editor = this.editor;
+
+ editor.selectAll();
+ this.$clickSelection = editor.getSelectionRange();
+ this.setState("null");
+ };
+
+ this.onMouseWheel = function(ev) {
+ if (ev.getShiftKey() || ev.getAccelKey()){
+ return;
+ }
+ var editor = this.editor;
+ var isScrolable = editor.renderer.isScrollableBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed);
+ if (isScrolable) {
+ this.$passScrollEvent = false;
+ } else {
+ if (this.$passScrollEvent)
+ return;
+
+ if (!this.$scrollStopTimeout) {
+ var self = this;
+ this.$scrollStopTimeout = setTimeout(function() {
+ self.$passScrollEvent = true;
+ self.$scrollStopTimeout = null;
+ }, 200);
+ }
+ }
+
+ editor.renderer.scrollBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed);
+ return ev.preventDefault();
+ };
+
+}).call(DefaultHandlers.prototype);
+
+exports.DefaultHandlers = DefaultHandlers;
+
+function calcDistance(ax, ay, bx, by) {
+ return Math.sqrt(Math.pow(bx - ax, 2) + Math.pow(by - ay, 2));
+}
+
+function calcRangeOrientation(range, cursor) {
+ if (range.start.row == range.end.row)
+ var cmp = 2 * cursor.column - range.start.column - range.end.column;
+ else
+ var cmp = 2 * cursor.row - range.start.row - range.end.row;
+
+ if (cmp < 0)
+ return {cursor: range.start, anchor: range.end};
+ else
+ return {cursor: range.end, anchor: range.start};
+}
+
+});
+
+ace.define('ace/mouse/default_gutter_handler', ['require', 'exports', 'module' , 'ace/lib/dom', 'ace/lib/event'], function(require, exports, module) {
+
+var dom = require("../lib/dom");
+var event = require("../lib/event");
+
+function GutterHandler(mouseHandler) {
+ var editor = mouseHandler.editor;
+ var gutter = editor.renderer.$gutterLayer;
+
+ mouseHandler.editor.setDefaultHandler("guttermousedown", function(e) {
+ if (!editor.isFocused())
+ return;
+ var gutterRegion = gutter.getRegion(e);
+
+ if (gutterRegion)
+ return;
+
+ var row = e.getDocumentPosition().row;
+ var selection = editor.session.selection;
+
+ if (e.getShiftKey())
+ selection.selectTo(row, 0);
+ else
+ mouseHandler.$clickSelection = editor.selection.getLineRange(row);
+
+ mouseHandler.captureMouse(e, "selectByLines");
+ return e.preventDefault();
+ });
+
+
+ var tooltipTimeout, mouseEvent, tooltip, tooltipAnnotation;
+ function createTooltip() {
+ tooltip = dom.createElement("div");
+ tooltip.className = "ace_gutter_tooltip";
+ tooltip.style.maxWidth = "500px";
+ tooltip.style.display = "none";
+ editor.container.appendChild(tooltip);
+ }
+
+ function showTooltip() {
+ if (!tooltip) {
+ createTooltip();
+ }
+ var row = mouseEvent.getDocumentPosition().row;
+ var annotation = gutter.$annotations[row];
+ if (!annotation)
+ return hideTooltip();
+
+ var maxRow = editor.session.getLength();
+ if (row == maxRow) {
+ var screenRow = editor.renderer.pixelToScreenCoordinates(0, mouseEvent.y).row;
+ var pos = mouseEvent.$pos;
+ if (screenRow > editor.session.documentToScreenRow(pos.row, pos.column))
+ return hideTooltip();
+ }
+
+ if (tooltipAnnotation == annotation)
+ return;
+ tooltipAnnotation = annotation.text.join("\n");
+
+ tooltip.style.display = "block";
+ tooltip.innerHTML = tooltipAnnotation;
+ editor.on("mousewheel", hideTooltip);
+
+ moveTooltip(mouseEvent);
+ }
+
+ function hideTooltip() {
+ if (tooltipTimeout)
+ tooltipTimeout = clearTimeout(tooltipTimeout);
+ if (tooltipAnnotation) {
+ tooltip.style.display = "none";
+ tooltipAnnotation = null;
+ editor.removeEventListener("mousewheel", hideTooltip);
+ }
+ }
+
+ function moveTooltip(e) {
+ var rect = editor.renderer.$gutter.getBoundingClientRect();
+ tooltip.style.left = e.x - rect.left + 15 + "px";
+ if (e.y + 3 * editor.renderer.lineHeight + 15 < rect.bottom) {
+ tooltip.style.bottom = "";
+ tooltip.style.top = e.y - rect.top + 15 + "px";
+ } else {
+ tooltip.style.top = "";
+ tooltip.style.bottom = rect.bottom - e.y + 5 + "px";
+ }
+ }
+
+ mouseHandler.editor.setDefaultHandler("guttermousemove", function(e) {
+ var target = e.domEvent.target || e.domEvent.srcElement;
+ if (dom.hasCssClass(target, "ace_fold-widget"))
+ return hideTooltip();
+
+ if (tooltipAnnotation)
+ moveTooltip(e);
+
+ mouseEvent = e;
+ if (tooltipTimeout)
+ return;
+ tooltipTimeout = setTimeout(function() {
+ tooltipTimeout = null;
+ if (mouseEvent)
+ showTooltip();
+ else
+ hideTooltip();
+ }, 50);
+ });
+
+ event.addListener(editor.renderer.$gutter, "mouseout", function(e) {
+ mouseEvent = null;
+ if (!tooltipAnnotation || tooltipTimeout)
+ return;
+
+ tooltipTimeout = setTimeout(function() {
+ tooltipTimeout = null;
+ hideTooltip();
+ }, 50);
+ });
+
+}
+
+exports.GutterHandler = GutterHandler;
+
+});
+
+ace.define('ace/mouse/mouse_event', ['require', 'exports', 'module' , 'ace/lib/event', 'ace/lib/useragent'], function(require, exports, module) {
+
+
+var event = require("../lib/event");
+var useragent = require("../lib/useragent");
+var MouseEvent = exports.MouseEvent = function(domEvent, editor) {
+ this.domEvent = domEvent;
+ this.editor = editor;
+
+ this.x = this.clientX = domEvent.clientX;
+ this.y = this.clientY = domEvent.clientY;
+
+ this.$pos = null;
+ this.$inSelection = null;
+
+ this.propagationStopped = false;
+ this.defaultPrevented = false;
+};
+
+(function() {
+
+ this.stopPropagation = function() {
+ event.stopPropagation(this.domEvent);
+ this.propagationStopped = true;
+ };
+
+ this.preventDefault = function() {
+ event.preventDefault(this.domEvent);
+ this.defaultPrevented = true;
+ };
+
+ this.stop = function() {
+ this.stopPropagation();
+ this.preventDefault();
+ };
+ this.getDocumentPosition = function() {
+ if (this.$pos)
+ return this.$pos;
+
+ this.$pos = this.editor.renderer.screenToTextCoordinates(this.clientX, this.clientY);
+ return this.$pos;
+ };
+ this.inSelection = function() {
+ if (this.$inSelection !== null)
+ return this.$inSelection;
+
+ var editor = this.editor;
+
+ if (editor.getReadOnly()) {
+ this.$inSelection = false;
+ }
+ else {
+ var selectionRange = editor.getSelectionRange();
+ if (selectionRange.isEmpty())
+ this.$inSelection = false;
+ else {
+ var pos = this.getDocumentPosition();
+ this.$inSelection = selectionRange.contains(pos.row, pos.column);
+ }
+ }
+ return this.$inSelection;
+ };
+ this.getButton = function() {
+ return event.getButton(this.domEvent);
+ };
+ this.getShiftKey = function() {
+ return this.domEvent.shiftKey;
+ };
+
+ this.getAccelKey = useragent.isMac
+ ? function() { return this.domEvent.metaKey; }
+ : function() { return this.domEvent.ctrlKey; };
+
+}).call(MouseEvent.prototype);
+
+});
+
+ace.define('ace/mouse/dragdrop', ['require', 'exports', 'module' , 'ace/lib/event'], function(require, exports, module) {
+
+
+var event = require("../lib/event");
+
+var DragdropHandler = function(mouseHandler) {
+ var editor = mouseHandler.editor;
+ var dragSelectionMarker, x, y;
+ var timerId, range, isBackwards;
+ var dragCursor, counter = 0;
+
+ var mouseTarget = editor.container;
+ event.addListener(mouseTarget, "dragenter", function(e) {
+ counter++;
+ if (!dragSelectionMarker) {
+ range = editor.getSelectionRange();
+ isBackwards = editor.selection.isBackwards();
+ var style = editor.getSelectionStyle();
+ dragSelectionMarker = editor.session.addMarker(range, "ace_selection", style);
+ editor.clearSelection();
+ clearInterval(timerId);
+ timerId = setInterval(onDragInterval, 20);
+ }
+ return event.preventDefault(e);
+ });
+
+ event.addListener(mouseTarget, "dragover", function(e) {
+ x = e.clientX;
+ y = e.clientY;
+ return event.preventDefault(e);
+ });
+
+ var onDragInterval = function() {
+ dragCursor = editor.renderer.screenToTextCoordinates(x, y);
+ editor.moveCursorToPosition(dragCursor);
+ editor.renderer.scrollCursorIntoView();
+ };
+
+ event.addListener(mouseTarget, "dragleave", function(e) {
+ counter--;
+ if (counter > 0)
+ return;
+ console.log(e.type, counter,e.target);
+ clearInterval(timerId);
+ editor.session.removeMarker(dragSelectionMarker);
+ dragSelectionMarker = null;
+ editor.selection.setSelectionRange(range, isBackwards);
+ return event.preventDefault(e);
+ });
+
+ event.addListener(mouseTarget, "drop", function(e) {
+ console.log(e.type, counter,e.target);
+ counter = 0;
+ clearInterval(timerId);
+ editor.session.removeMarker(dragSelectionMarker);
+ dragSelectionMarker = null;
+
+ range.end = editor.session.insert(dragCursor, e.dataTransfer.getData('Text'));
+ range.start = dragCursor;
+ editor.focus();
+ editor.selection.setSelectionRange(range);
+ return event.preventDefault(e);
+ });
+
+};
+
+exports.DragdropHandler = DragdropHandler;
+});
+
+ace.define('ace/mouse/fold_handler', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+function FoldHandler(editor) {
+
+ editor.on("click", function(e) {
+ var position = e.getDocumentPosition();
+ var session = editor.session;
+
+ // If the user clicked on a fold, then expand it.
+ var fold = session.getFoldAt(position.row, position.column, 1);
+ if (fold) {
+ if (e.getAccelKey())
+ session.removeFold(fold);
+ else
+ session.expandFold(fold);
+
+ e.stop();
+ }
+ });
+
+ editor.on("gutterclick", function(e) {
+ var gutterRegion = editor.renderer.$gutterLayer.getRegion(e);
+
+ if (gutterRegion == "foldWidgets") {
+ var row = e.getDocumentPosition().row;
+ var session = editor.session;
+ if (session.foldWidgets && session.foldWidgets[row])
+ editor.session.onFoldWidgetClick(row, e);
+ e.stop();
+ }
+ });
+}
+
+exports.FoldHandler = FoldHandler;
+
+});
+
+ace.define('ace/keyboard/keybinding', ['require', 'exports', 'module' , 'ace/lib/keys', 'ace/lib/event'], function(require, exports, module) {
+
+
+var keyUtil = require("../lib/keys");
+var event = require("../lib/event");
+
+var KeyBinding = function(editor) {
+ this.$editor = editor;
+ this.$data = { };
+ this.$handlers = [];
+ this.setDefaultHandler(editor.commands);
+};
+
+(function() {
+ this.setDefaultHandler = function(kb) {
+ this.removeKeyboardHandler(this.$defaultHandler);
+ this.$defaultHandler = kb;
+ this.addKeyboardHandler(kb, 0);
+ this.$data = {editor: this.$editor};
+ };
+
+ this.setKeyboardHandler = function(kb) {
+ if (this.$handlers[this.$handlers.length - 1] == kb)
+ return;
+
+ while (this.$handlers[1])
+ this.removeKeyboardHandler(this.$handlers[1]);
+
+ this.addKeyboardHandler(kb, 1);
+ };
+
+ this.addKeyboardHandler = function(kb, pos) {
+ if (!kb)
+ return;
+ var i = this.$handlers.indexOf(kb);
+ if (i != -1)
+ this.$handlers.splice(i, 1);
+
+ if (pos == undefined)
+ this.$handlers.push(kb);
+ else
+ this.$handlers.splice(pos, 0, kb);
+
+ if (i == -1 && kb.attach)
+ kb.attach(this.$editor);
+ };
+
+ this.removeKeyboardHandler = function(kb) {
+ var i = this.$handlers.indexOf(kb);
+ if (i == -1)
+ return false;
+ this.$handlers.splice(i, 1);
+ kb.detach && kb.detach(this.$editor);
+ return true;
+ };
+
+ this.getKeyboardHandler = function() {
+ return this.$handlers[this.$handlers.length - 1];
+ };
+
+ this.$callKeyboardHandlers = function (hashId, keyString, keyCode, e) {
+ var toExecute;
+ for (var i = this.$handlers.length; i--;) {
+ toExecute = this.$handlers[i].handleKeyboard(
+ this.$data, hashId, keyString, keyCode, e
+ );
+ if (toExecute && toExecute.command)
+ break;
+ }
+
+ if (!toExecute || !toExecute.command)
+ return false;
+
+ var success = false;
+ var commands = this.$editor.commands;
+
+ // allow keyboardHandler to consume keys
+ if (toExecute.command != "null")
+ success = commands.exec(toExecute.command, this.$editor, toExecute.args, e);
+ else
+ success = toExecute.passEvent != true;
+
+ // do not stop input events to not break repeating
+ if (success && e && hashId != -1)
+ event.stopEvent(e);
+
+ return success;
+ };
+
+ this.onCommandKey = function(e, hashId, keyCode) {
+ var keyString = keyUtil.keyCodeToString(keyCode);
+ this.$callKeyboardHandlers(hashId, keyString, keyCode, e);
+ };
+
+ this.onTextInput = function(text) {
+ var success = this.$callKeyboardHandlers(-1, text);
+ if (!success)
+ this.$editor.commands.exec("insertstring", this.$editor, text);
+ };
+
+}).call(KeyBinding.prototype);
+
+exports.KeyBinding = KeyBinding;
+});
+
+ace.define('ace/edit_session', ['require', 'exports', 'module' , 'ace/config', 'ace/lib/oop', 'ace/lib/lang', 'ace/lib/net', 'ace/lib/event_emitter', 'ace/selection', 'ace/mode/text', 'ace/range', 'ace/document', 'ace/background_tokenizer', 'ace/search_highlight', 'ace/edit_session/folding', 'ace/edit_session/bracket_match'], function(require, exports, module) {
+
+
+var config = require("./config");
+var oop = require("./lib/oop");
+var lang = require("./lib/lang");
+var net = require("./lib/net");
+var EventEmitter = require("./lib/event_emitter").EventEmitter;
+var Selection = require("./selection").Selection;
+var TextMode = require("./mode/text").Mode;
+var Range = require("./range").Range;
+var Document = require("./document").Document;
+var BackgroundTokenizer = require("./background_tokenizer").BackgroundTokenizer;
+var SearchHighlight = require("./search_highlight").SearchHighlight;
+
+// events
+/**
+ * EditSession@change(e)
+ * - e (Object): An object containing a `delta` of information about the change.
+ *
+ * Emitted when the document changes.
+ **/
+/**
+ * EditSession@changeTabSize()
+ *
+ * Emitted when the tab size changes, via [[EditSession.setTabSize]].
+ **/
+/**
+ * EditSession@changeOverwrite()
+ *
+ * Emitted when the ability to overwrite text changes, via [[EditSession.setOverwrite]].
+ **/
+/**
+ * EditSession@changeBreakpoint()
+ *
+ * Emitted when the gutter changes, either by setting or removing breakpoints, or when the gutter decorations change.
+ **/
+/**
+ * EditSession@changeFrontMarker()
+ *
+ * Emitted when a front marker changes.
+ **/
+/**
+ * EditSession@changeBackMarker()
+ *
+ * Emitted when a back marker changes.
+ **/
+/**
+ * EditSession@changeAnnotation()
+ *
+ * Emitted when an annotation changes, like through [[EditSession.setAnnotations]].
+ **/
+/**
+ * EditSession@tokenizerUpdate(e)
+ * - e (Object): An object containing one property, `"data"`, that contains information about the changing rows
+ *
+ * Emitted when a background tokenizer asynchronously processes new rows.
+ *
+ **/
+/** hide
+ * EditSession@loadMode(e)
+ *
+ *
+ *
+ **/
+/**
+ * EditSession@changeMode()
+ *
+ * Emitted when the current mode changes.
+ *
+ **/
+/**
+ * EditSession@changeWrapMode()
+ *
+ * Emitted when the wrap mode changes.
+ *
+ **/
+/**
+ * EditSession@changeWrapLimit()
+ *
+ * Emitted when the wrapping limit changes.
+ *
+ **/
+/**
+ * EditSession@changeFold(e)
+ *
+ * Emitted when a code fold is added or removed.
+ *
+ **/
+ /**
+ * EditSession@changeScrollTop(scrollTop)
+ * - scrollTop (Number): The new scroll top value
+ *
+ * Emitted when the scroll top changes.
+ **/
+/**
+ * EditSession@changeScrollLeft(scrollLeft)
+ * - scrollLeft (Number): The new scroll left value
+ *
+ * Emitted when the scroll left changes.
+ **/
+
+
+/**
+ * new EditSession(text, mode)
+ * - text (Document | String): If `text` is a `Document`, it associates the `EditSession` with it. Otherwise, a new `Document` is created, with the initial text
+ * - mode (TextMode): The inital language mode to use for the document
+ *
+ * Sets up a new `EditSession` and associates it with the given `Document` and `TextMode`.
+ *
+ **/
+
+var EditSession = function(text, mode) {
+ this.$modified = true;
+ this.$breakpoints = [];
+ this.$decorations = [];
+ this.$frontMarkers = {};
+ this.$backMarkers = {};
+ this.$markerId = 1;
+ this.$resetRowCache(0);
+ this.$wrapData = [];
+ this.$foldData = [];
+ this.$rowLengthCache = [];
+ this.$undoSelect = true;
+ this.$foldData.toString = function() {
+ var str = "";
+ this.forEach(function(foldLine) {
+ str += "\n" + foldLine.toString();
+ });
+ return str;
+ }
+
+ if (typeof text == "object" && text.getLine) {
+ this.setDocument(text);
+ } else {
+ this.setDocument(new Document(text));
+ }
+
+ this.selection = new Selection(this);
+ this.setMode(mode);
+};
+
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+ this.setDocument = function(doc) {
+ if (this.doc)
+ throw new Error("Document is already set");
+
+ this.doc = doc;
+ doc.on("change", this.onChange.bind(this));
+ this.on("changeFold", this.onChangeFold.bind(this));
+
+ if (this.bgTokenizer) {
+ this.bgTokenizer.setDocument(this.getDocument());
+ this.bgTokenizer.start(0);
+ }
+ };
+ this.getDocument = function() {
+ return this.doc;
+ };
+ this.$resetRowCache = function(docRow) {
+ if (!docRow) {
+ this.$docRowCache = [];
+ this.$screenRowCache = [];
+ return;
+ }
+
+ var i = this.$getRowCacheIndex(this.$docRowCache, docRow) + 1;
+ var l = this.$docRowCache.length;
+ this.$docRowCache.splice(i, l);
+ this.$screenRowCache.splice(i, l);
+
+ };
+
+ this.$getRowCacheIndex = function(cacheArray, val) {
+ var low = 0;
+ var hi = cacheArray.length - 1;
+
+ while (low <= hi) {
+ var mid = (low + hi) >> 1;
+ var c = cacheArray[mid];
+
+ if (val > c)
+ low = mid + 1;
+ else if (val < c)
+ hi = mid - 1;
+ else
+ return mid;
+ }
+
+ return low && low -1;
+ };
+
+ this.onChangeFold = function(e) {
+ var fold = e.data;
+ this.$resetRowCache(fold.start.row);
+ };
+
+ this.onChange = function(e) {
+ var delta = e.data;
+ this.$modified = true;
+
+ this.$resetRowCache(delta.range.start.row);
+
+ var removedFolds = this.$updateInternalDataOnChange(e);
+ if (!this.$fromUndo && this.$undoManager && !delta.ignore) {
+ this.$deltasDoc.push(delta);
+ if (removedFolds && removedFolds.length != 0) {
+ this.$deltasFold.push({
+ action: "removeFolds",
+ folds: removedFolds
+ });
+ }
+
+ this.$informUndoManager.schedule();
+ }
+
+ this.bgTokenizer.$updateOnChange(delta);
+ this._emit("change", e);
+ };
+ this.setValue = function(text) {
+ this.doc.setValue(text);
+ this.selection.moveCursorTo(0, 0);
+ this.selection.clearSelection();
+
+ this.$resetRowCache(0);
+ this.$deltas = [];
+ this.$deltasDoc = [];
+ this.$deltasFold = [];
+ this.getUndoManager().reset();
+ };
+ /** alias of: EditSession.getValue
+ * EditSession.toString() -> String
+ *
+ * Returns the current [[Document `Document`]] as a string.
+ *
+ **/
+ this.getValue =
+ this.toString = function() {
+ return this.doc.getValue();
+ };
+ this.getSelection = function() {
+ return this.selection;
+ };
+ this.getState = function(row) {
+ return this.bgTokenizer.getState(row);
+ };
+ this.getTokens = function(row) {
+ return this.bgTokenizer.getTokens(row);
+ };
+ this.getTokenAt = function(row, column) {
+ var tokens = this.bgTokenizer.getTokens(row);
+ var token, c = 0;
+ if (column == null) {
+ i = tokens.length - 1;
+ c = this.getLine(row).length;
+ } else {
+ for (var i = 0; i < tokens.length; i++) {
+ c += tokens[i].value.length;
+ if (c >= column)
+ break;
+ }
+ }
+ token = tokens[i];
+ if (!token)
+ return null;
+ token.index = i;
+ token.start = c - token.value.length;
+ return token;
+ };
+
+ this.highlight = function(re) {
+ if (!this.$searchHighlight) {
+ var highlight = new SearchHighlight(null, "ace_selected_word", "text");
+ this.$searchHighlight = this.addDynamicMarker(highlight);
+ }
+ this.$searchHighlight.setRegexp(re);
+ }
+ /**
+ * EditSession.setUndoManager(undoManager)
+ * - undoManager (UndoManager): The new undo manager
+ *
+ * Sets the undo manager.
+ **/
+ this.setUndoManager = function(undoManager) {
+ this.$undoManager = undoManager;
+ this.$deltas = [];
+ this.$deltasDoc = [];
+ this.$deltasFold = [];
+
+ if (this.$informUndoManager)
+ this.$informUndoManager.cancel();
+
+ if (undoManager) {
+ var self = this;
+ this.$syncInformUndoManager = function() {
+ self.$informUndoManager.cancel();
+
+ if (self.$deltasFold.length) {
+ self.$deltas.push({
+ group: "fold",
+ deltas: self.$deltasFold
+ });
+ self.$deltasFold = [];
+ }
+
+ if (self.$deltasDoc.length) {
+ self.$deltas.push({
+ group: "doc",
+ deltas: self.$deltasDoc
+ });
+ self.$deltasDoc = [];
+ }
+
+ if (self.$deltas.length > 0) {
+ undoManager.execute({
+ action: "aceupdate",
+ args: [self.$deltas, self]
+ });
+ }
+
+ self.$deltas = [];
+ }
+ this.$informUndoManager =
+ lang.deferredCall(this.$syncInformUndoManager);
+ }
+ };
+
+ this.$defaultUndoManager = {
+ undo: function() {},
+ redo: function() {},
+ reset: function() {}
+ };
+ this.getUndoManager = function() {
+ return this.$undoManager || this.$defaultUndoManager;
+ },
+
+ /**
+ * EditSession.getTabString() -> String
+ *
+ * Returns the current value for tabs. If the user is using soft tabs, this will be a series of spaces (defined by [[EditSession.getTabSize `getTabSize()`]]); otherwise it's simply `'\t'`.
+ **/
+ this.getTabString = function() {
+ if (this.getUseSoftTabs()) {
+ return lang.stringRepeat(" ", this.getTabSize());
+ } else {
+ return "\t";
+ }
+ };
+
+ this.$useSoftTabs = true;
+ this.setUseSoftTabs = function(useSoftTabs) {
+ if (this.$useSoftTabs === useSoftTabs) return;
+
+ this.$useSoftTabs = useSoftTabs;
+ };
+ this.getUseSoftTabs = function() {
+ return this.$useSoftTabs;
+ };
+
+ this.$tabSize = 4;
+ this.setTabSize = function(tabSize) {
+ if (isNaN(tabSize) || this.$tabSize === tabSize) return;
+
+ this.$modified = true;
+ this.$rowLengthCache = [];
+ this.$tabSize = tabSize;
+ this._emit("changeTabSize");
+ };
+ this.getTabSize = function() {
+ return this.$tabSize;
+ };
+ this.isTabStop = function(position) {
+ return this.$useSoftTabs && (position.column % this.$tabSize == 0);
+ };
+
+ this.$overwrite = false;
+ this.setOverwrite = function(overwrite) {
+ if (this.$overwrite == overwrite) return;
+
+ this.$overwrite = overwrite;
+ this._emit("changeOverwrite");
+ };
+ this.getOverwrite = function() {
+ return this.$overwrite;
+ };
+ this.toggleOverwrite = function() {
+ this.setOverwrite(!this.$overwrite);
+ };
+ this.addGutterDecoration = function(row, className) {
+ if (!this.$decorations[row])
+ this.$decorations[row] = "";
+ this.$decorations[row] += " " + className;
+ this._emit("changeBreakpoint", {});
+ };
+ this.removeGutterDecoration = function(row, className) {
+ this.$decorations[row] = (this.$decorations[row] || "").replace(" " + className, "");
+ this._emit("changeBreakpoint", {});
+ };
+ this.getBreakpoints = function() {
+ return this.$breakpoints;
+ };
+ this.setBreakpoints = function(rows) {
+ this.$breakpoints = [];
+ for (var i=0; i 0)
+ inToken = !!line.charAt(column - 1).match(this.tokenRe);
+
+ if (!inToken)
+ inToken = !!line.charAt(column).match(this.tokenRe);
+
+ if (inToken)
+ var re = this.tokenRe;
+ else if (/^\s+$/.test(line.slice(column-1, column+1)))
+ var re = /\s/;
+ else
+ var re = this.nonTokenRe;
+
+ var start = column;
+ if (start > 0) {
+ do {
+ start--;
+ }
+ while (start >= 0 && line.charAt(start).match(re));
+ start++;
+ }
+
+ var end = column;
+ while (end < line.length && line.charAt(end).match(re)) {
+ end++;
+ }
+
+ return new Range(row, start, row, end);
+ };
+ this.getAWordRange = function(row, column) {
+ var wordRange = this.getWordRange(row, column);
+ var line = this.getLine(wordRange.end.row);
+
+ while (line.charAt(wordRange.end.column).match(/[ \t]/)) {
+ wordRange.end.column += 1;
+ }
+ return wordRange;
+ };
+ this.setNewLineMode = function(newLineMode) {
+ this.doc.setNewLineMode(newLineMode);
+ };
+ this.getNewLineMode = function() {
+ return this.doc.getNewLineMode();
+ };
+
+ this.$useWorker = true;
+ this.setUseWorker = function(useWorker) {
+ if (this.$useWorker == useWorker)
+ return;
+
+ this.$useWorker = useWorker;
+
+ this.$stopWorker();
+ if (useWorker)
+ this.$startWorker();
+ };
+ this.getUseWorker = function() {
+ return this.$useWorker;
+ };
+ this.onReloadTokenizer = function(e) {
+ var rows = e.data;
+ this.bgTokenizer.start(rows.first);
+ this._emit("tokenizerUpdate", e);
+ };
+
+ this.$modes = {};
+ this._loadMode = function(mode, callback) {
+ if (!this.$modes["null"])
+ this.$modes["null"] = this.$modes["ace/mode/text"] = new TextMode();
+
+ if (this.$modes[mode])
+ return callback(this.$modes[mode]);
+
+ var _self = this;
+ var module;
+ try {
+ module = require(mode);
+ } catch (e) {};
+ // sometimes require returns empty object (this bug is present in requirejs 2 as well)
+ if (module && module.Mode)
+ return done(module);
+
+ // set mode to text until loading is finished
+ if (!this.$mode)
+ this.$setModePlaceholder();
+
+ fetch(mode, function() {
+ require([mode], done);
+ });
+
+ function done(module) {
+ if (_self.$modes[mode])
+ return callback(_self.$modes[mode]);
+
+ _self.$modes[mode] = new module.Mode();
+ _self.$modes[mode].$id = mode;
+ _self._emit("loadmode", {
+ name: mode,
+ mode: _self.$modes[mode]
+ });
+ callback(_self.$modes[mode]);
+ }
+
+ function fetch(name, callback) {
+ if (!config.get("packaged"))
+ return callback();
+
+ net.loadScript(config.moduleUrl(name, "mode"), callback);
+ }
+ };
+
+ this.$setModePlaceholder = function() {
+ this.$mode = this.$modes["null"];
+ var tokenizer = this.$mode.getTokenizer();
+
+ if (!this.bgTokenizer) {
+ this.bgTokenizer = new BackgroundTokenizer(tokenizer);
+ var _self = this;
+ this.bgTokenizer.addEventListener("update", function(e) {
+ _self._emit("tokenizerUpdate", e);
+ });
+ } else {
+ this.bgTokenizer.setTokenizer(tokenizer);
+ }
+ this.bgTokenizer.setDocument(this.getDocument());
+
+ this.tokenRe = this.$mode.tokenRe;
+ this.nonTokenRe = this.$mode.nonTokenRe;
+ };
+ this.$mode = null;
+ this.$modeId = null;
+ this.setMode = function(mode) {
+ mode = mode || "null";
+ // load on demand
+ if (typeof mode === "string") {
+ if (this.$modeId == mode)
+ return;
+
+ this.$modeId = mode;
+ var _self = this;
+ this._loadMode(mode, function(module) {
+ if (_self.$modeId !== mode)
+ return;
+
+ _self.setMode(module);
+ });
+ return;
+ }
+
+ if (this.$mode === mode) return;
+ this.$mode = mode;
+ this.$modeId = mode.$id;
+
+ this.$stopWorker();
+
+ if (this.$useWorker)
+ this.$startWorker();
+
+ var tokenizer = mode.getTokenizer();
+
+ if(tokenizer.addEventListener !== undefined) {
+ var onReloadTokenizer = this.onReloadTokenizer.bind(this);
+ tokenizer.addEventListener("update", onReloadTokenizer);
+ }
+
+ if (!this.bgTokenizer) {
+ this.bgTokenizer = new BackgroundTokenizer(tokenizer);
+ var _self = this;
+ this.bgTokenizer.addEventListener("update", function(e) {
+ _self._emit("tokenizerUpdate", e);
+ });
+ } else {
+ this.bgTokenizer.setTokenizer(tokenizer);
+ }
+
+ this.bgTokenizer.setDocument(this.getDocument());
+ this.bgTokenizer.start(0);
+
+ this.tokenRe = mode.tokenRe;
+ this.nonTokenRe = mode.nonTokenRe;
+
+ this.$setFolding(mode.foldingRules);
+
+ this._emit("changeMode");
+ };
+ this.$stopWorker = function() {
+ if (this.$worker)
+ this.$worker.terminate();
+
+ this.$worker = null;
+ };
+ this.$startWorker = function() {
+ if (typeof Worker !== "undefined" && !require.noWorker) {
+ try {
+ this.$worker = this.$mode.createWorker(this);
+ } catch (e) {
+ console.log("Could not load worker");
+ console.log(e);
+ this.$worker = null;
+ }
+ }
+ else
+ this.$worker = null;
+ };
+ this.getMode = function() {
+ return this.$mode;
+ };
+
+ this.$scrollTop = 0;
+ this.setScrollTop = function(scrollTop) {
+ scrollTop = Math.round(Math.max(0, scrollTop));
+ if (this.$scrollTop === scrollTop)
+ return;
+
+ this.$scrollTop = scrollTop;
+ this._emit("changeScrollTop", scrollTop);
+ };
+ this.getScrollTop = function() {
+ return this.$scrollTop;
+ };
+
+ this.$scrollLeft = 0;
+ this.setScrollLeft = function(scrollLeft) {
+ scrollLeft = Math.round(Math.max(0, scrollLeft));
+ if (this.$scrollLeft === scrollLeft)
+ return;
+
+ this.$scrollLeft = scrollLeft;
+ this._emit("changeScrollLeft", scrollLeft);
+ };
+ this.getScrollLeft = function() {
+ return this.$scrollLeft;
+ };
+ this.getScreenWidth = function() {
+ this.$computeWidth();
+ return this.screenWidth;
+ };
+
+ this.$computeWidth = function(force) {
+ if (this.$modified || force) {
+ this.$modified = false;
+
+ if (this.$useWrapMode)
+ return this.screenWidth = this.$wrapLimit;
+
+ var lines = this.doc.getAllLines();
+ var cache = this.$rowLengthCache;
+ var longestScreenLine = 0;
+ var foldIndex = 0;
+ var foldLine = this.$foldData[foldIndex];
+ var foldStart = foldLine ? foldLine.start.row : Infinity;
+ var len = lines.length;
+
+ for (var i = 0; i < len; i++) {
+ if (i > foldStart) {
+ i = foldLine.end.row + 1;
+ if (i >= len)
+ break;
+ foldLine = this.$foldData[foldIndex++];
+ foldStart = foldLine ? foldLine.start.row : Infinity;
+ }
+
+ if (cache[i] == null)
+ cache[i] = this.$getStringScreenWidth(lines[i])[0];
+
+ if (cache[i] > longestScreenLine)
+ longestScreenLine = cache[i];
+ }
+ this.screenWidth = longestScreenLine;
+ }
+ };
+ this.getLine = function(row) {
+ return this.doc.getLine(row);
+ };
+ this.getLines = function(firstRow, lastRow) {
+ return this.doc.getLines(firstRow, lastRow);
+ };
+ this.getLength = function() {
+ return this.doc.getLength();
+ };
+ this.getTextRange = function(range) {
+ return this.doc.getTextRange(range || this.selection.getRange());
+ };
+ this.insert = function(position, text) {
+ return this.doc.insert(position, text);
+ };
+ this.remove = function(range) {
+ return this.doc.remove(range);
+ };
+ this.undoChanges = function(deltas, dontSelect) {
+ if (!deltas.length)
+ return;
+
+ this.$fromUndo = true;
+ var lastUndoRange = null;
+ for (var i = deltas.length - 1; i != -1; i--) {
+ var delta = deltas[i];
+ if (delta.group == "doc") {
+ this.doc.revertDeltas(delta.deltas);
+ lastUndoRange =
+ this.$getUndoSelection(delta.deltas, true, lastUndoRange);
+ } else {
+ delta.deltas.forEach(function(foldDelta) {
+ this.addFolds(foldDelta.folds);
+ }, this);
+ }
+ }
+ this.$fromUndo = false;
+ lastUndoRange &&
+ this.$undoSelect &&
+ !dontSelect &&
+ this.selection.setSelectionRange(lastUndoRange);
+ return lastUndoRange;
+ };
+ this.redoChanges = function(deltas, dontSelect) {
+ if (!deltas.length)
+ return;
+
+ this.$fromUndo = true;
+ var lastUndoRange = null;
+ for (var i = 0; i < deltas.length; i++) {
+ var delta = deltas[i];
+ if (delta.group == "doc") {
+ this.doc.applyDeltas(delta.deltas);
+ lastUndoRange =
+ this.$getUndoSelection(delta.deltas, false, lastUndoRange);
+ }
+ }
+ this.$fromUndo = false;
+ lastUndoRange &&
+ this.$undoSelect &&
+ !dontSelect &&
+ this.selection.setSelectionRange(lastUndoRange);
+ return lastUndoRange;
+ };
+ this.setUndoSelect = function(enable) {
+ this.$undoSelect = enable;
+ };
+ this.$getUndoSelection = function(deltas, isUndo, lastUndoRange) {
+ function isInsert(delta) {
+ var insert =
+ delta.action == "insertText" || delta.action == "insertLines";
+ return isUndo ? !insert : insert;
+ }
+
+ var delta = deltas[0];
+ var range, point;
+ var lastDeltaIsInsert = false;
+ if (isInsert(delta)) {
+ range = delta.range.clone();
+ lastDeltaIsInsert = true;
+ } else {
+ range = Range.fromPoints(delta.range.start, delta.range.start);
+ lastDeltaIsInsert = false;
+ }
+
+ for (var i = 1; i < deltas.length; i++) {
+ delta = deltas[i];
+ if (isInsert(delta)) {
+ point = delta.range.start;
+ if (range.compare(point.row, point.column) == -1) {
+ range.setStart(delta.range.start);
+ }
+ point = delta.range.end;
+ if (range.compare(point.row, point.column) == 1) {
+ range.setEnd(delta.range.end);
+ }
+ lastDeltaIsInsert = true;
+ } else {
+ point = delta.range.start;
+ if (range.compare(point.row, point.column) == -1) {
+ range =
+ Range.fromPoints(delta.range.start, delta.range.start);
+ }
+ lastDeltaIsInsert = false;
+ }
+ }
+
+ // Check if this range and the last undo range has something in common.
+ // If true, merge the ranges.
+ if (lastUndoRange != null) {
+ var cmp = lastUndoRange.compareRange(range);
+ if (cmp == 1) {
+ range.setStart(lastUndoRange.start);
+ } else if (cmp == -1) {
+ range.setEnd(lastUndoRange.end);
+ }
+ }
+
+ return range;
+ },
+
+ /** related to: Document.replace
+ * EditSession.replace(range, text) -> Object
+ * - range (Range): A specified Range to replace
+ * - text (String): The new text to use as a replacement
+ * + (Object): Returns an object containing the final row and column, like this:
+ * ```{row: endRow, column: 0}```
+ * If the text and range are empty, this function returns an object containing the current `range.start` value.
+ * If the text is the exact same as what currently exists, this function returns an object containing the current `range.end` value.
+ *
+ * Replaces a range in the document with the new `text`.
+ *
+ *
+ *
+ **/
+ this.replace = function(range, text) {
+ return this.doc.replace(range, text);
+ };
+ this.moveText = function(fromRange, toPosition) {
+ var text = this.getTextRange(fromRange);
+ this.remove(fromRange);
+
+ var toRow = toPosition.row;
+ var toColumn = toPosition.column;
+
+ // Make sure to update the insert location, when text is removed in
+ // front of the chosen point of insertion.
+ if (!fromRange.isMultiLine() && fromRange.start.row == toRow &&
+ fromRange.end.column < toColumn)
+ toColumn -= text.length;
+
+ if (fromRange.isMultiLine() && fromRange.end.row < toRow) {
+ var lines = this.doc.$split(text);
+ toRow -= lines.length - 1;
+ }
+
+ var endRow = toRow + fromRange.end.row - fromRange.start.row;
+ var endColumn = fromRange.isMultiLine() ?
+ fromRange.end.column :
+ toColumn + fromRange.end.column - fromRange.start.column;
+
+ var toRange = new Range(toRow, toColumn, endRow, endColumn);
+
+ this.insert(toRange.start, text);
+
+ return toRange;
+ };
+ this.indentRows = function(startRow, endRow, indentString) {
+ indentString = indentString.replace(/\t/g, this.getTabString());
+ for (var row=startRow; row<=endRow; row++)
+ this.insert({row: row, column:0}, indentString);
+ };
+ this.outdentRows = function (range) {
+ var rowRange = range.collapseRows();
+ var deleteRange = new Range(0, 0, 0, 0);
+ var size = this.getTabSize();
+
+ for (var i = rowRange.start.row; i <= rowRange.end.row; ++i) {
+ var line = this.getLine(i);
+
+ deleteRange.start.row = i;
+ deleteRange.end.row = i;
+ for (var j = 0; j < size; ++j)
+ if (line.charAt(j) != ' ')
+ break;
+ if (j < size && line.charAt(j) == '\t') {
+ deleteRange.start.column = j;
+ deleteRange.end.column = j + 1;
+ } else {
+ deleteRange.start.column = 0;
+ deleteRange.end.column = j;
+ }
+ this.remove(deleteRange);
+ }
+ };
+ this.moveLinesUp = function(firstRow, lastRow) {
+ if (firstRow <= 0) return 0;
+
+ var removed = this.doc.removeLines(firstRow, lastRow);
+ this.doc.insertLines(firstRow - 1, removed);
+ return -1;
+ };
+ this.moveLinesDown = function(firstRow, lastRow) {
+ if (lastRow >= this.doc.getLength()-1) return 0;
+
+ var removed = this.doc.removeLines(firstRow, lastRow);
+ this.doc.insertLines(firstRow+1, removed);
+ return 1;
+ };
+ this.duplicateLines = function(firstRow, lastRow) {
+ var firstRow = this.$clipRowToDocument(firstRow);
+ var lastRow = this.$clipRowToDocument(lastRow);
+
+ var lines = this.getLines(firstRow, lastRow);
+ this.doc.insertLines(firstRow, lines);
+
+ var addedRows = lastRow - firstRow + 1;
+ return addedRows;
+ };
+
+
+ this.$clipRowToDocument = function(row) {
+ return Math.max(0, Math.min(row, this.doc.getLength()-1));
+ };
+
+ this.$clipColumnToRow = function(row, column) {
+ if (column < 0)
+ return 0;
+ return Math.min(this.doc.getLine(row).length, column);
+ };
+
+
+ this.$clipPositionToDocument = function(row, column) {
+ column = Math.max(0, column);
+
+ if (row < 0) {
+ row = 0;
+ column = 0;
+ } else {
+ var len = this.doc.getLength();
+ if (row >= len) {
+ row = len - 1;
+ column = this.doc.getLine(len-1).length;
+ } else {
+ column = Math.min(this.doc.getLine(row).length, column);
+ }
+ }
+
+ return {
+ row: row,
+ column: column
+ };
+ };
+
+ this.$clipRangeToDocument = function(range) {
+ if (range.start.row < 0) {
+ range.start.row = 0;
+ range.start.column = 0;
+ } else {
+ range.start.column = this.$clipColumnToRow(
+ range.start.row,
+ range.start.column
+ );
+ }
+
+ var len = this.doc.getLength() - 1;
+ if (range.end.row > len) {
+ range.end.row = len;
+ range.end.column = this.doc.getLine(len).length;
+ } else {
+ range.end.column = this.$clipColumnToRow(
+ range.end.row,
+ range.end.column
+ );
+ }
+ return range;
+ };
+
+ // WRAPMODE
+ this.$wrapLimit = 80;
+ this.$useWrapMode = false;
+ this.$wrapLimitRange = {
+ min : null,
+ max : null
+ };
+ this.setUseWrapMode = function(useWrapMode) {
+ if (useWrapMode != this.$useWrapMode) {
+ this.$useWrapMode = useWrapMode;
+ this.$modified = true;
+ this.$resetRowCache(0);
+
+ // If wrapMode is activaed, the wrapData array has to be initialized.
+ if (useWrapMode) {
+ var len = this.getLength();
+ this.$wrapData = [];
+ for (var i = 0; i < len; i++) {
+ this.$wrapData.push([]);
+ }
+ this.$updateWrapData(0, len - 1);
+ }
+
+ this._emit("changeWrapMode");
+ }
+ };
+ this.getUseWrapMode = function() {
+ return this.$useWrapMode;
+ };
+
+ // Allow the wrap limit to move freely between min and max. Either
+ // parameter can be null to allow the wrap limit to be unconstrained
+ // in that direction. Or set both parameters to the same number to pin
+ // the limit to that value.
+ /**
+ * EditSession.setWrapLimitRange(min, max)
+ * - min (Number): The minimum wrap value (the left side wrap)
+ * - max (Number): The maximum wrap value (the right side wrap)
+ *
+ * Sets the boundaries of wrap. Either value can be `null` to have an unconstrained wrap, or, they can be the same number to pin the limit. If the wrap limits for `min` or `max` are different, this method also emits the `'changeWrapMode'` event.
+ **/
+ this.setWrapLimitRange = function(min, max) {
+ if (this.$wrapLimitRange.min !== min || this.$wrapLimitRange.max !== max) {
+ this.$wrapLimitRange.min = min;
+ this.$wrapLimitRange.max = max;
+ this.$modified = true;
+ // This will force a recalculation of the wrap limit
+ this._emit("changeWrapMode");
+ }
+ };
+ this.adjustWrapLimit = function(desiredLimit) {
+ var wrapLimit = this.$constrainWrapLimit(desiredLimit);
+ if (wrapLimit != this.$wrapLimit && wrapLimit > 0) {
+ this.$wrapLimit = wrapLimit;
+ this.$modified = true;
+ if (this.$useWrapMode) {
+ this.$updateWrapData(0, this.getLength() - 1);
+ this.$resetRowCache(0);
+ this._emit("changeWrapLimit");
+ }
+ return true;
+ }
+ return false;
+ };
+ this.$constrainWrapLimit = function(wrapLimit) {
+ var min = this.$wrapLimitRange.min;
+ if (min)
+ wrapLimit = Math.max(min, wrapLimit);
+
+ var max = this.$wrapLimitRange.max;
+ if (max)
+ wrapLimit = Math.min(max, wrapLimit);
+
+ // What would a limit of 0 even mean?
+ return Math.max(1, wrapLimit);
+ };
+ this.getWrapLimit = function() {
+ return this.$wrapLimit;
+ };
+ this.getWrapLimitRange = function() {
+ // Avoid unexpected mutation by returning a copy
+ return {
+ min : this.$wrapLimitRange.min,
+ max : this.$wrapLimitRange.max
+ };
+ };
+ this.$updateInternalDataOnChange = function(e) {
+ var useWrapMode = this.$useWrapMode;
+ var len;
+ var action = e.data.action;
+ var firstRow = e.data.range.start.row;
+ var lastRow = e.data.range.end.row;
+ var start = e.data.range.start;
+ var end = e.data.range.end;
+ var removedFolds = null;
+
+ if (action.indexOf("Lines") != -1) {
+ if (action == "insertLines") {
+ lastRow = firstRow + (e.data.lines.length);
+ } else {
+ lastRow = firstRow;
+ }
+ len = e.data.lines ? e.data.lines.length : lastRow - firstRow;
+ } else {
+ len = lastRow - firstRow;
+ }
+
+ if (len != 0) {
+ if (action.indexOf("remove") != -1) {
+ this[useWrapMode ? "$wrapData" : "$rowLengthCache"].splice(firstRow, len);
+
+ var foldLines = this.$foldData;
+ removedFolds = this.getFoldsInRange(e.data.range);
+ this.removeFolds(removedFolds);
+
+ var foldLine = this.getFoldLine(end.row);
+ var idx = 0;
+ if (foldLine) {
+ foldLine.addRemoveChars(end.row, end.column, start.column - end.column);
+ foldLine.shiftRow(-len);
+
+ var foldLineBefore = this.getFoldLine(firstRow);
+ if (foldLineBefore && foldLineBefore !== foldLine) {
+ foldLineBefore.merge(foldLine);
+ foldLine = foldLineBefore;
+ }
+ idx = foldLines.indexOf(foldLine) + 1;
+ }
+
+ for (idx; idx < foldLines.length; idx++) {
+ var foldLine = foldLines[idx];
+ if (foldLine.start.row >= end.row) {
+ foldLine.shiftRow(-len);
+ }
+ }
+
+ lastRow = firstRow;
+ } else {
+ var args;
+ if (useWrapMode) {
+ args = [firstRow, 0];
+ for (var i = 0; i < len; i++) args.push([]);
+ this.$wrapData.splice.apply(this.$wrapData, args);
+ } else {
+ args = Array(len);
+ args.unshift(firstRow, 0);
+ this.$rowLengthCache.splice.apply(this.$rowLengthCache, args);
+ }
+
+ // If some new line is added inside of a foldLine, then split
+ // the fold line up.
+ var foldLines = this.$foldData;
+ var foldLine = this.getFoldLine(firstRow);
+ var idx = 0;
+ if (foldLine) {
+ var cmp = foldLine.range.compareInside(start.row, start.column)
+ // Inside of the foldLine range. Need to split stuff up.
+ if (cmp == 0) {
+ foldLine = foldLine.split(start.row, start.column);
+ foldLine.shiftRow(len);
+ foldLine.addRemoveChars(
+ lastRow, 0, end.column - start.column);
+ } else
+ // Infront of the foldLine but same row. Need to shift column.
+ if (cmp == -1) {
+ foldLine.addRemoveChars(firstRow, 0, end.column - start.column);
+ foldLine.shiftRow(len);
+ }
+ // Nothing to do if the insert is after the foldLine.
+ idx = foldLines.indexOf(foldLine) + 1;
+ }
+
+ for (idx; idx < foldLines.length; idx++) {
+ var foldLine = foldLines[idx];
+ if (foldLine.start.row >= firstRow) {
+ foldLine.shiftRow(len);
+ }
+ }
+ }
+ } else {
+ // Realign folds. E.g. if you add some new chars before a fold, the
+ // fold should "move" to the right.
+ len = Math.abs(e.data.range.start.column - e.data.range.end.column);
+ if (action.indexOf("remove") != -1) {
+ // Get all the folds in the change range and remove them.
+ removedFolds = this.getFoldsInRange(e.data.range);
+ this.removeFolds(removedFolds);
+
+ len = -len;
+ }
+ var foldLine = this.getFoldLine(firstRow);
+ if (foldLine) {
+ foldLine.addRemoveChars(firstRow, start.column, len);
+ }
+ }
+
+ if (useWrapMode && this.$wrapData.length != this.doc.getLength()) {
+ console.error("doc.getLength() and $wrapData.length have to be the same!");
+ }
+
+ if (useWrapMode)
+ this.$updateWrapData(firstRow, lastRow);
+ else
+ this.$updateRowLengthCache(firstRow, lastRow);
+
+ return removedFolds;
+ };
+
+ this.$updateRowLengthCache = function(firstRow, lastRow, b) {
+ this.$rowLengthCache[firstRow] = null;
+ this.$rowLengthCache[lastRow] = null;
+ };
+ this.$updateWrapData = function(firstRow, lastRow) {
+ var lines = this.doc.getAllLines();
+ var tabSize = this.getTabSize();
+ var wrapData = this.$wrapData;
+ var wrapLimit = this.$wrapLimit;
+ var tokens;
+ var foldLine;
+
+ var row = firstRow;
+ lastRow = Math.min(lastRow, lines.length - 1);
+ while (row <= lastRow) {
+ foldLine = this.getFoldLine(row, foldLine);
+ if (!foldLine) {
+ tokens = this.$getDisplayTokens(lang.stringTrimRight(lines[row]));
+ wrapData[row] = this.$computeWrapSplits(tokens, wrapLimit, tabSize);
+ row ++;
+ } else {
+ tokens = [];
+ foldLine.walk(
+ function(placeholder, row, column, lastColumn) {
+ var walkTokens;
+ if (placeholder) {
+ walkTokens = this.$getDisplayTokens(
+ placeholder, tokens.length);
+ walkTokens[0] = PLACEHOLDER_START;
+ for (var i = 1; i < walkTokens.length; i++) {
+ walkTokens[i] = PLACEHOLDER_BODY;
+ }
+ } else {
+ walkTokens = this.$getDisplayTokens(
+ lines[row].substring(lastColumn, column),
+ tokens.length);
+ }
+ tokens = tokens.concat(walkTokens);
+ }.bind(this),
+ foldLine.end.row,
+ lines[foldLine.end.row].length + 1
+ );
+ // Remove spaces/tabs from the back of the token array.
+ while (tokens.length != 0 && tokens[tokens.length - 1] >= SPACE)
+ tokens.pop();
+
+ wrapData[foldLine.start.row]
+ = this.$computeWrapSplits(tokens, wrapLimit, tabSize);
+ row = foldLine.end.row + 1;
+ }
+ }
+ };
+
+ // "Tokens"
+ var CHAR = 1,
+ CHAR_EXT = 2,
+ PLACEHOLDER_START = 3,
+ PLACEHOLDER_BODY = 4,
+ PUNCTUATION = 9,
+ SPACE = 10,
+ TAB = 11,
+ TAB_SPACE = 12;
+ this.$computeWrapSplits = function(tokens, wrapLimit) {
+ if (tokens.length == 0) {
+ return [];
+ }
+
+ var splits = [];
+ var displayLength = tokens.length;
+ var lastSplit = 0, lastDocSplit = 0;
+
+ function addSplit(screenPos) {
+ var displayed = tokens.slice(lastSplit, screenPos);
+
+ // The document size is the current size - the extra width for tabs
+ // and multipleWidth characters.
+ var len = displayed.length;
+ displayed.join("").
+ // Get all the TAB_SPACEs.
+ replace(/12/g, function() {
+ len -= 1;
+ }).
+ // Get all the CHAR_EXT/multipleWidth characters.
+ replace(/2/g, function() {
+ len -= 1;
+ });
+
+ lastDocSplit += len;
+ splits.push(lastDocSplit);
+ lastSplit = screenPos;
+ }
+
+ while (displayLength - lastSplit > wrapLimit) {
+ // This is, where the split should be.
+ var split = lastSplit + wrapLimit;
+
+ // If there is a space or tab at this split position, then making
+ // a split is simple.
+ if (tokens[split] >= SPACE) {
+ // Include all following spaces + tabs in this split as well.
+ while (tokens[split] >= SPACE) {
+ split ++;
+ }
+ addSplit(split);
+ continue;
+ }
+
+ // === ELSE ===
+ // Check if split is inside of a placeholder. Placeholder are
+ // not splitable. Therefore, seek the beginning of the placeholder
+ // and try to place the split beofre the placeholder's start.
+ if (tokens[split] == PLACEHOLDER_START
+ || tokens[split] == PLACEHOLDER_BODY)
+ {
+ // Seek the start of the placeholder and do the split
+ // before the placeholder. By definition there always
+ // a PLACEHOLDER_START between split and lastSplit.
+ for (split; split != lastSplit - 1; split--) {
+ if (tokens[split] == PLACEHOLDER_START) {
+ // split++; << No incremental here as we want to
+ // have the position before the Placeholder.
+ break;
+ }
+ }
+
+ // If the PLACEHOLDER_START is not the index of the
+ // last split, then we can do the split
+ if (split > lastSplit) {
+ addSplit(split);
+ continue;
+ }
+
+ // If the PLACEHOLDER_START IS the index of the last
+ // split, then we have to place the split after the
+ // placeholder. So, let's seek for the end of the placeholder.
+ split = lastSplit + wrapLimit;
+ for (split; split < tokens.length; split++) {
+ if (tokens[split] != PLACEHOLDER_BODY)
+ {
+ break;
+ }
+ }
+
+ // If spilt == tokens.length, then the placeholder is the last
+ // thing in the line and adding a new split doesn't make sense.
+ if (split == tokens.length) {
+ break; // Breaks the while-loop.
+ }
+
+ // Finally, add the split...
+ addSplit(split);
+ continue;
+ }
+
+ // === ELSE ===
+ // Search for the first non space/tab/placeholder/punctuation token backwards.
+ var minSplit = Math.max(split - 10, lastSplit - 1);
+ while (split > minSplit && tokens[split] < PLACEHOLDER_START) {
+ split --;
+ }
+ while (split > minSplit && tokens[split] == PUNCTUATION) {
+ split --;
+ }
+ // If we found one, then add the split.
+ if (split > minSplit) {
+ addSplit(++split);
+ continue;
+ }
+
+ // === ELSE ===
+ split = lastSplit + wrapLimit;
+ // The split is inside of a CHAR or CHAR_EXT token and no space
+ // around -> force a split.
+ addSplit(split);
+ }
+ return splits;
+ };
+ this.$getDisplayTokens = function(str, offset) {
+ var arr = [];
+ var tabSize;
+ offset = offset || 0;
+
+ for (var i = 0; i < str.length; i++) {
+ var c = str.charCodeAt(i);
+ // Tab
+ if (c == 9) {
+ tabSize = this.getScreenTabSize(arr.length + offset);
+ arr.push(TAB);
+ for (var n = 1; n < tabSize; n++) {
+ arr.push(TAB_SPACE);
+ }
+ }
+ // Space
+ else if (c == 32) {
+ arr.push(SPACE);
+ } else if((c > 39 && c < 48) || (c > 57 && c < 64)) {
+ arr.push(PUNCTUATION);
+ }
+ // full width characters
+ else if (c >= 0x1100 && isFullWidth(c)) {
+ arr.push(CHAR, CHAR_EXT);
+ } else {
+ arr.push(CHAR);
+ }
+ }
+ return arr;
+ };
+ this.$getStringScreenWidth = function(str, maxScreenColumn, screenColumn) {
+ if (maxScreenColumn == 0)
+ return [0, 0];
+ if (maxScreenColumn == null)
+ maxScreenColumn = Infinity;
+ screenColumn = screenColumn || 0;
+
+ var c, column;
+ for (column = 0; column < str.length; column++) {
+ c = str.charCodeAt(column);
+ // tab
+ if (c == 9) {
+ screenColumn += this.getScreenTabSize(screenColumn);
+ }
+ // full width characters
+ else if (c >= 0x1100 && isFullWidth(c)) {
+ screenColumn += 2;
+ } else {
+ screenColumn += 1;
+ }
+ if (screenColumn > maxScreenColumn) {
+ break;
+ }
+ }
+
+ return [screenColumn, column];
+ };
+ this.getRowLength = function(row) {
+ if (!this.$useWrapMode || !this.$wrapData[row]) {
+ return 1;
+ } else {
+ return this.$wrapData[row].length + 1;
+ }
+ };
+ this.getScreenLastRowColumn = function(screenRow) {
+ var pos = this.screenToDocumentPosition(screenRow, Number.MAX_VALUE);
+ return this.documentToScreenColumn(pos.row, pos.column);
+ };
+ this.getDocumentLastRowColumn = function(docRow, docColumn) {
+ var screenRow = this.documentToScreenRow(docRow, docColumn);
+ return this.getScreenLastRowColumn(screenRow);
+ };
+ this.getDocumentLastRowColumnPosition = function(docRow, docColumn) {
+ var screenRow = this.documentToScreenRow(docRow, docColumn);
+ return this.screenToDocumentPosition(screenRow, Number.MAX_VALUE / 10);
+ };
+ this.getRowSplitData = function(row) {
+ if (!this.$useWrapMode) {
+ return undefined;
+ } else {
+ return this.$wrapData[row];
+ }
+ };
+ this.getScreenTabSize = function(screenColumn) {
+ return this.$tabSize - screenColumn % this.$tabSize;
+ };
+ this.screenToDocumentRow = function(screenRow, screenColumn) {
+ return this.screenToDocumentPosition(screenRow, screenColumn).row;
+ };
+ this.screenToDocumentColumn = function(screenRow, screenColumn) {
+ return this.screenToDocumentPosition(screenRow, screenColumn).column;
+ };
+ this.screenToDocumentPosition = function(screenRow, screenColumn) {
+ if (screenRow < 0)
+ return {row: 0, column: 0};
+
+ var line;
+ var docRow = 0;
+ var docColumn = 0;
+ var column;
+ var row = 0;
+ var rowLength = 0;
+
+ var rowCache = this.$screenRowCache;
+ var i = this.$getRowCacheIndex(rowCache, screenRow);
+ if (0 < i && i < rowCache.length) {
+ var row = rowCache[i];
+ var docRow = this.$docRowCache[i];
+ var doCache = screenRow > row || (screenRow == row && i == rowCache.length - 1);
+ } else {
+ var doCache = i != 0 || !rowCache.length;
+ }
+
+ var maxRow = this.getLength() - 1;
+ var foldLine = this.getNextFoldLine(docRow);
+ var foldStart = foldLine ? foldLine.start.row : Infinity;
+
+ while (row <= screenRow) {
+ rowLength = this.getRowLength(docRow);
+ if (row + rowLength - 1 >= screenRow || docRow >= maxRow) {
+ break;
+ } else {
+ row += rowLength;
+ docRow++;
+ if (docRow > foldStart) {
+ docRow = foldLine.end.row+1;
+ foldLine = this.getNextFoldLine(docRow, foldLine);
+ foldStart = foldLine ? foldLine.start.row : Infinity;
+ }
+ }
+ if (doCache) {
+ this.$docRowCache.push(docRow);
+ this.$screenRowCache.push(row);
+ }
+ }
+
+ if (foldLine && foldLine.start.row <= docRow) {
+ line = this.getFoldDisplayLine(foldLine);
+ docRow = foldLine.start.row;
+ } else if (row + rowLength <= screenRow || docRow > maxRow) {
+ // clip at the end of the document
+ return {
+ row: maxRow,
+ column: this.getLine(maxRow).length
+ }
+ } else {
+ line = this.getLine(docRow);
+ foldLine = null;
+ }
+
+ if (this.$useWrapMode) {
+ var splits = this.$wrapData[docRow];
+ if (splits) {
+ column = splits[screenRow - row];
+ if(screenRow > row && splits.length) {
+ docColumn = splits[screenRow - row - 1] || splits[splits.length - 1];
+ line = line.substring(docColumn);
+ }
+ }
+ }
+
+ docColumn += this.$getStringScreenWidth(line, screenColumn)[1];
+
+ // We remove one character at the end so that the docColumn
+ // position returned is not associated to the next row on the screen.
+ if (this.$useWrapMode && docColumn >= column)
+ docColumn = column - 1;
+
+ if (foldLine)
+ return foldLine.idxToPosition(docColumn);
+
+ return {row: docRow, column: docColumn};
+ };
+ this.documentToScreenPosition = function(docRow, docColumn) {
+ // Normalize the passed in arguments.
+ if (typeof docColumn === "undefined")
+ var pos = this.$clipPositionToDocument(docRow.row, docRow.column);
+ else
+ pos = this.$clipPositionToDocument(docRow, docColumn);
+
+ docRow = pos.row;
+ docColumn = pos.column;
+
+ var screenRow = 0;
+ var foldStartRow = null;
+ var fold = null;
+
+ // Clamp the docRow position in case it's inside of a folded block.
+ fold = this.getFoldAt(docRow, docColumn, 1);
+ if (fold) {
+ docRow = fold.start.row;
+ docColumn = fold.start.column;
+ }
+
+ var rowEnd, row = 0;
+
+
+ var rowCache = this.$docRowCache;
+ var i = this.$getRowCacheIndex(rowCache, docRow);
+ if (0 < i && i < rowCache.length) {
+ var row = rowCache[i];
+ var screenRow = this.$screenRowCache[i];
+ var doCache = docRow > row || (docRow == row && i == rowCache.length - 1);
+ } else {
+ var doCache = i != 0 || !rowCache.length;
+ }
+
+ var foldLine = this.getNextFoldLine(row);
+ var foldStart = foldLine ?foldLine.start.row :Infinity;
+
+ while (row < docRow) {
+ if (row >= foldStart) {
+ rowEnd = foldLine.end.row + 1;
+ if (rowEnd > docRow)
+ break;
+ foldLine = this.getNextFoldLine(rowEnd, foldLine);
+ foldStart = foldLine ?foldLine.start.row :Infinity;
+ }
+ else {
+ rowEnd = row + 1;
+ }
+
+ screenRow += this.getRowLength(row);
+ row = rowEnd;
+
+ if (doCache) {
+ this.$docRowCache.push(row);
+ this.$screenRowCache.push(screenRow);
+ }
+ }
+
+ // Calculate the text line that is displayed in docRow on the screen.
+ var textLine = "";
+ // Check if the final row we want to reach is inside of a fold.
+ if (foldLine && row >= foldStart) {
+ textLine = this.getFoldDisplayLine(foldLine, docRow, docColumn);
+ foldStartRow = foldLine.start.row;
+ } else {
+ textLine = this.getLine(docRow).substring(0, docColumn);
+ foldStartRow = docRow;
+ }
+ // Clamp textLine if in wrapMode.
+ if (this.$useWrapMode) {
+ var wrapRow = this.$wrapData[foldStartRow];
+ var screenRowOffset = 0;
+ while (textLine.length >= wrapRow[screenRowOffset]) {
+ screenRow ++;
+ screenRowOffset++;
+ }
+ textLine = textLine.substring(
+ wrapRow[screenRowOffset - 1] || 0, textLine.length
+ );
+ }
+
+ return {
+ row: screenRow,
+ column: this.$getStringScreenWidth(textLine)[0]
+ };
+ };
+ this.documentToScreenColumn = function(row, docColumn) {
+ return this.documentToScreenPosition(row, docColumn).column;
+ };
+ this.documentToScreenRow = function(docRow, docColumn) {
+ return this.documentToScreenPosition(docRow, docColumn).row;
+ };
+ this.getScreenLength = function() {
+ var screenRows = 0;
+ var fold = null;
+ if (!this.$useWrapMode) {
+ screenRows = this.getLength();
+
+ // Remove the folded lines again.
+ var foldData = this.$foldData;
+ for (var i = 0; i < foldData.length; i++) {
+ fold = foldData[i];
+ screenRows -= fold.end.row - fold.start.row;
+ }
+ } else {
+ var lastRow = this.$wrapData.length;
+ var row = 0, i = 0;
+ var fold = this.$foldData[i++];
+ var foldStart = fold ? fold.start.row :Infinity;
+
+ while (row < lastRow) {
+ screenRows += this.$wrapData[row].length + 1;
+ row ++;
+ if (row > foldStart) {
+ row = fold.end.row+1;
+ fold = this.$foldData[i++];
+ foldStart = fold ?fold.start.row :Infinity;
+ }
+ }
+ }
+
+ return screenRows;
+ }
+
+ // For every keystroke this gets called once per char in the whole doc!!
+ // Wouldn't hurt to make it a bit faster for c >= 0x1100
+ function isFullWidth(c) {
+ if (c < 0x1100)
+ return false;
+ return c >= 0x1100 && c <= 0x115F ||
+ c >= 0x11A3 && c <= 0x11A7 ||
+ c >= 0x11FA && c <= 0x11FF ||
+ c >= 0x2329 && c <= 0x232A ||
+ c >= 0x2E80 && c <= 0x2E99 ||
+ c >= 0x2E9B && c <= 0x2EF3 ||
+ c >= 0x2F00 && c <= 0x2FD5 ||
+ c >= 0x2FF0 && c <= 0x2FFB ||
+ c >= 0x3000 && c <= 0x303E ||
+ c >= 0x3041 && c <= 0x3096 ||
+ c >= 0x3099 && c <= 0x30FF ||
+ c >= 0x3105 && c <= 0x312D ||
+ c >= 0x3131 && c <= 0x318E ||
+ c >= 0x3190 && c <= 0x31BA ||
+ c >= 0x31C0 && c <= 0x31E3 ||
+ c >= 0x31F0 && c <= 0x321E ||
+ c >= 0x3220 && c <= 0x3247 ||
+ c >= 0x3250 && c <= 0x32FE ||
+ c >= 0x3300 && c <= 0x4DBF ||
+ c >= 0x4E00 && c <= 0xA48C ||
+ c >= 0xA490 && c <= 0xA4C6 ||
+ c >= 0xA960 && c <= 0xA97C ||
+ c >= 0xAC00 && c <= 0xD7A3 ||
+ c >= 0xD7B0 && c <= 0xD7C6 ||
+ c >= 0xD7CB && c <= 0xD7FB ||
+ c >= 0xF900 && c <= 0xFAFF ||
+ c >= 0xFE10 && c <= 0xFE19 ||
+ c >= 0xFE30 && c <= 0xFE52 ||
+ c >= 0xFE54 && c <= 0xFE66 ||
+ c >= 0xFE68 && c <= 0xFE6B ||
+ c >= 0xFF01 && c <= 0xFF60 ||
+ c >= 0xFFE0 && c <= 0xFFE6;
+ };
+
+}).call(EditSession.prototype);
+
+require("./edit_session/folding").Folding.call(EditSession.prototype);
+require("./edit_session/bracket_match").BracketMatch.call(EditSession.prototype);
+
+exports.EditSession = EditSession;
+});
+
+ace.define('ace/config', ['require', 'exports', 'module' , 'ace/lib/lang'], function(require, exports, module) {
+"no use strict";
+
+var lang = require("./lib/lang");
+
+var global = (function() {
+ return this;
+})();
+
+var options = {
+ packaged: false,
+ workerPath: "",
+ modePath: "",
+ themePath: "",
+ suffix: ".js",
+ $moduleUrls: {}
+};
+
+exports.get = function(key) {
+ if (!options.hasOwnProperty(key))
+ throw new Error("Unknown config key: " + key);
+
+ return options[key];
+};
+
+exports.set = function(key, value) {
+ if (!options.hasOwnProperty(key))
+ throw new Error("Unknown config key: " + key);
+
+ options[key] = value;
+};
+
+exports.all = function() {
+ return lang.copyObject(options);
+};
+
+exports.moduleUrl = function(name, component) {
+ if (options.$moduleUrls[name])
+ return options.$moduleUrls[name];
+
+ var parts = name.split("/");
+ component = component || parts[parts.length - 2] || "";
+ var base = parts[parts.length - 1].replace(component, "").replace(/(^[\-_])|([\-_]$)/, "");
+
+ if (!base && parts.length > 1)
+ base = parts[parts.length - 2];
+ return this.get(component + "Path") + "/" + component + "-" + base + this.get("suffix");
+};
+
+exports.setModuleUrl = function(name, subst) {
+ return options.$moduleUrls[name] = subst;
+};
+
+exports.init = function() {
+ options.packaged = require.packaged || module.packaged || (global.define && define.packaged);
+
+ if (!global.document)
+ return "";
+
+ var scriptOptions = {};
+ var scriptUrl = "";
+
+ var scripts = document.getElementsByTagName("script");
+ for (var i=0; i lead.row || (anchor.row == lead.row && anchor.column > lead.column));
+ };
+ this.getRange = function() {
+ var anchor = this.anchor;
+ var lead = this.lead;
+
+ if (this.isEmpty())
+ return Range.fromPoints(lead, lead);
+
+ if (this.isBackwards()) {
+ return Range.fromPoints(lead, anchor);
+ }
+ else {
+ return Range.fromPoints(anchor, lead);
+ }
+ };
+ this.clearSelection = function() {
+ if (!this.$isEmpty) {
+ this.$isEmpty = true;
+ this._emit("changeSelection");
+ }
+ };
+ this.selectAll = function() {
+ var lastRow = this.doc.getLength() - 1;
+ this.setSelectionAnchor(0, 0);
+ this.moveCursorTo(lastRow, this.doc.getLine(lastRow).length);
+ };
+ this.setRange =
+ this.setSelectionRange = function(range, reverse) {
+ if (reverse) {
+ this.setSelectionAnchor(range.end.row, range.end.column);
+ this.selectTo(range.start.row, range.start.column);
+ } else {
+ this.setSelectionAnchor(range.start.row, range.start.column);
+ this.selectTo(range.end.row, range.end.column);
+ }
+ this.$desiredColumn = null;
+ };
+
+ this.$moveSelection = function(mover) {
+ var lead = this.lead;
+ if (this.$isEmpty)
+ this.setSelectionAnchor(lead.row, lead.column);
+
+ mover.call(this);
+ };
+ this.selectTo = function(row, column) {
+ this.$moveSelection(function() {
+ this.moveCursorTo(row, column);
+ });
+ };
+ this.selectToPosition = function(pos) {
+ this.$moveSelection(function() {
+ this.moveCursorToPosition(pos);
+ });
+ };
+ this.selectUp = function() {
+ this.$moveSelection(this.moveCursorUp);
+ };
+ this.selectDown = function() {
+ this.$moveSelection(this.moveCursorDown);
+ };
+ this.selectRight = function() {
+ this.$moveSelection(this.moveCursorRight);
+ };
+ this.selectLeft = function() {
+ this.$moveSelection(this.moveCursorLeft);
+ };
+ this.selectLineStart = function() {
+ this.$moveSelection(this.moveCursorLineStart);
+ };
+ this.selectLineEnd = function() {
+ this.$moveSelection(this.moveCursorLineEnd);
+ };
+ this.selectFileEnd = function() {
+ this.$moveSelection(this.moveCursorFileEnd);
+ };
+ this.selectFileStart = function() {
+ this.$moveSelection(this.moveCursorFileStart);
+ };
+ this.selectWordRight = function() {
+ this.$moveSelection(this.moveCursorWordRight);
+ };
+ this.selectWordLeft = function() {
+ this.$moveSelection(this.moveCursorWordLeft);
+ };
+ this.getWordRange = function(row, column) {
+ if (typeof column == "undefined") {
+ var cursor = row || this.lead;
+ row = cursor.row;
+ column = cursor.column;
+ }
+ return this.session.getWordRange(row, column);
+ };
+
+ this.selectWord = function() {
+ this.setSelectionRange(this.getWordRange());
+ };
+ this.selectAWord = function() {
+ var cursor = this.getCursor();
+ var range = this.session.getAWordRange(cursor.row, cursor.column);
+ this.setSelectionRange(range);
+ };
+
+ this.getLineRange = function(row, excludeLastChar) {
+ var rowStart = typeof row == "number" ? row : this.lead.row;
+ var rowEnd;
+
+ var foldLine = this.session.getFoldLine(rowStart);
+ if (foldLine) {
+ rowStart = foldLine.start.row;
+ rowEnd = foldLine.end.row;
+ } else {
+ rowEnd = rowStart;
+ }
+ if (excludeLastChar)
+ return new Range(rowStart, 0, rowEnd, this.session.getLine(rowEnd).length);
+ else
+ return new Range(rowStart, 0, rowEnd + 1, 0);
+ };
+ this.selectLine = function() {
+ this.setSelectionRange(this.getLineRange());
+ };
+ this.moveCursorUp = function() {
+ this.moveCursorBy(-1, 0);
+ };
+ this.moveCursorDown = function() {
+ this.moveCursorBy(1, 0);
+ };
+ this.moveCursorLeft = function() {
+ var cursor = this.lead.getPosition(),
+ fold;
+
+ if (fold = this.session.getFoldAt(cursor.row, cursor.column, -1)) {
+ this.moveCursorTo(fold.start.row, fold.start.column);
+ } else if (cursor.column == 0) {
+ // cursor is a line (start
+ if (cursor.row > 0) {
+ this.moveCursorTo(cursor.row - 1, this.doc.getLine(cursor.row - 1).length);
+ }
+ }
+ else {
+ var tabSize = this.session.getTabSize();
+ if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column-tabSize, cursor.column).split(" ").length-1 == tabSize)
+ this.moveCursorBy(0, -tabSize);
+ else
+ this.moveCursorBy(0, -1);
+ }
+ };
+ this.moveCursorRight = function() {
+ var cursor = this.lead.getPosition(),
+ fold;
+ if (fold = this.session.getFoldAt(cursor.row, cursor.column, 1)) {
+ this.moveCursorTo(fold.end.row, fold.end.column);
+ }
+ else if (this.lead.column == this.doc.getLine(this.lead.row).length) {
+ if (this.lead.row < this.doc.getLength() - 1) {
+ this.moveCursorTo(this.lead.row + 1, 0);
+ }
+ }
+ else {
+ var tabSize = this.session.getTabSize();
+ var cursor = this.lead;
+ if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column, cursor.column+tabSize).split(" ").length-1 == tabSize)
+ this.moveCursorBy(0, tabSize);
+ else
+ this.moveCursorBy(0, 1);
+ }
+ };
+ this.moveCursorLineStart = function() {
+ var row = this.lead.row;
+ var column = this.lead.column;
+ var screenRow = this.session.documentToScreenRow(row, column);
+
+ // Determ the doc-position of the first character at the screen line.
+ var firstColumnPosition = this.session.screenToDocumentPosition(screenRow, 0);
+
+ // Determ the line
+ var beforeCursor = this.session.getDisplayLine(
+ row, null, firstColumnPosition.row,
+ firstColumnPosition.column
+ );
+
+ var leadingSpace = beforeCursor.match(/^\s*/);
+ if (leadingSpace[0].length == column) {
+ this.moveCursorTo(
+ firstColumnPosition.row, firstColumnPosition.column
+ );
+ }
+ else {
+ this.moveCursorTo(
+ firstColumnPosition.row,
+ firstColumnPosition.column + leadingSpace[0].length
+ );
+ }
+ };
+ this.moveCursorLineEnd = function() {
+ var lead = this.lead;
+ var lineEnd = this.session.getDocumentLastRowColumnPosition(lead.row, lead.column);
+ if (this.lead.column == lineEnd.column) {
+ var line = this.session.getLine(lineEnd.row);
+ if (lineEnd.column == line.length) {
+ var textEnd = line.search(/\s+$/);
+ if (textEnd > 0)
+ lineEnd.column = textEnd;
+ }
+ }
+
+ this.moveCursorTo(lineEnd.row, lineEnd.column);
+ };
+ this.moveCursorFileEnd = function() {
+ var row = this.doc.getLength() - 1;
+ var column = this.doc.getLine(row).length;
+ this.moveCursorTo(row, column);
+ };
+ this.moveCursorFileStart = function() {
+ this.moveCursorTo(0, 0);
+ };
+ this.moveCursorLongWordRight = function() {
+ var row = this.lead.row;
+ var column = this.lead.column;
+ var line = this.doc.getLine(row);
+ var rightOfCursor = line.substring(column);
+
+ var match;
+ this.session.nonTokenRe.lastIndex = 0;
+ this.session.tokenRe.lastIndex = 0;
+
+ // skip folds
+ var fold = this.session.getFoldAt(row, column, 1);
+ if (fold) {
+ this.moveCursorTo(fold.end.row, fold.end.column);
+ return;
+ }
+
+ // first skip space
+ if (match = this.session.nonTokenRe.exec(rightOfCursor)) {
+ column += this.session.nonTokenRe.lastIndex;
+ this.session.nonTokenRe.lastIndex = 0;
+ rightOfCursor = line.substring(column);
+ }
+
+ // if at line end proceed with next line
+ if (column >= line.length) {
+ this.moveCursorTo(row, line.length);
+ this.moveCursorRight();
+ if (row < this.doc.getLength() - 1)
+ this.moveCursorWordRight();
+ return;
+ }
+
+ // advance to the end of the next token
+ if (match = this.session.tokenRe.exec(rightOfCursor)) {
+ column += this.session.tokenRe.lastIndex;
+ this.session.tokenRe.lastIndex = 0;
+ }
+
+ this.moveCursorTo(row, column);
+ };
+ this.moveCursorLongWordLeft = function() {
+ var row = this.lead.row;
+ var column = this.lead.column;
+
+ // skip folds
+ var fold;
+ if (fold = this.session.getFoldAt(row, column, -1)) {
+ this.moveCursorTo(fold.start.row, fold.start.column);
+ return;
+ }
+
+ var str = this.session.getFoldStringAt(row, column, -1);
+ if (str == null) {
+ str = this.doc.getLine(row).substring(0, column)
+ }
+
+ var leftOfCursor = lang.stringReverse(str);
+ var match;
+ this.session.nonTokenRe.lastIndex = 0;
+ this.session.tokenRe.lastIndex = 0;
+
+ // skip whitespace
+ if (match = this.session.nonTokenRe.exec(leftOfCursor)) {
+ column -= this.session.nonTokenRe.lastIndex;
+ leftOfCursor = leftOfCursor.slice(this.session.nonTokenRe.lastIndex);
+ this.session.nonTokenRe.lastIndex = 0;
+ }
+
+ // if at begin of the line proceed in line above
+ if (column <= 0) {
+ this.moveCursorTo(row, 0);
+ this.moveCursorLeft();
+ if (row > 0)
+ this.moveCursorWordLeft();
+ return;
+ }
+
+ // move to the begin of the word
+ if (match = this.session.tokenRe.exec(leftOfCursor)) {
+ column -= this.session.tokenRe.lastIndex;
+ this.session.tokenRe.lastIndex = 0;
+ }
+
+ this.moveCursorTo(row, column);
+ };
+
+ this.$shortWordEndIndex = function(rightOfCursor) {
+ var match, index = 0, ch;
+ var whitespaceRe = /\s/;
+ var tokenRe = this.session.tokenRe;
+
+ tokenRe.lastIndex = 0;
+ if (match = this.session.tokenRe.exec(rightOfCursor)) {
+ index = this.session.tokenRe.lastIndex;
+ } else {
+ while ((ch = rightOfCursor[index]) && whitespaceRe.test(ch))
+ index ++;
+
+ if (index <= 1) {
+ tokenRe.lastIndex = 0;
+ while ((ch = rightOfCursor[index]) && !tokenRe.test(ch)) {
+ tokenRe.lastIndex = 0;
+ index ++;
+ if (whitespaceRe.test(ch)) {
+ if (index > 2) {
+ index--
+ break;
+ } else {
+ while ((ch = rightOfCursor[index]) && whitespaceRe.test(ch))
+ index ++;
+ if (index > 2)
+ break
+ }
+ }
+ }
+ }
+ }
+ tokenRe.lastIndex = 0;
+
+ return index;
+ };
+
+ this.moveCursorShortWordRight = function() {
+ var row = this.lead.row;
+ var column = this.lead.column;
+ var line = this.doc.getLine(row);
+ var rightOfCursor = line.substring(column);
+
+ var fold = this.session.getFoldAt(row, column, 1);
+ if (fold)
+ return this.moveCursorTo(fold.end.row, fold.end.column);
+
+ if (column == line.length) {
+ var l = this.doc.getLength();
+ do {
+ row++;
+ rightOfCursor = this.doc.getLine(row)
+ } while (row < l && /^\s*$/.test(rightOfCursor))
+
+ if (!/^\s+/.test(rightOfCursor))
+ rightOfCursor = ""
+ column = 0;
+ }
+
+ var index = this.$shortWordEndIndex(rightOfCursor);
+
+ this.moveCursorTo(row, column + index);
+ };
+
+ this.moveCursorShortWordLeft = function() {
+ var row = this.lead.row;
+ var column = this.lead.column;
+
+ var fold;
+ if (fold = this.session.getFoldAt(row, column, -1))
+ return this.moveCursorTo(fold.start.row, fold.start.column);
+
+ var line = this.session.getLine(row).substring(0, column);
+ if (column == 0) {
+ do {
+ row--;
+ line = this.doc.getLine(row);
+ } while (row > 0 && /^\s*$/.test(line))
+
+ column = line.length;
+ if (!/\s+$/.test(line))
+ line = ""
+ }
+
+ var leftOfCursor = lang.stringReverse(line);
+ var index = this.$shortWordEndIndex(leftOfCursor);
+
+ return this.moveCursorTo(row, column - index);
+ };
+
+ this.moveCursorWordRight = function() {
+ if (this.session.$selectLongWords)
+ this.moveCursorLongWordRight();
+ else
+ this.moveCursorShortWordRight();
+ };
+
+ this.moveCursorWordLeft = function() {
+ if (this.session.$selectLongWords)
+ this.moveCursorLongWordLeft();
+ else
+ this.moveCursorShortWordLeft();
+ };
+ this.moveCursorBy = function(rows, chars) {
+ var screenPos = this.session.documentToScreenPosition(
+ this.lead.row,
+ this.lead.column
+ );
+
+ if (chars === 0) {
+ if (this.$desiredColumn)
+ screenPos.column = this.$desiredColumn;
+ else
+ this.$desiredColumn = screenPos.column;
+ }
+
+ var docPos = this.session.screenToDocumentPosition(screenPos.row + rows, screenPos.column);
+
+ // move the cursor and update the desired column
+ this.moveCursorTo(docPos.row, docPos.column + chars, chars === 0);
+ };
+ this.moveCursorToPosition = function(position) {
+ this.moveCursorTo(position.row, position.column);
+ };
+ this.moveCursorTo = function(row, column, keepDesiredColumn) {
+ // Ensure the row/column is not inside of a fold.
+ var fold = this.session.getFoldAt(row, column, 1);
+ if (fold) {
+ row = fold.start.row;
+ column = fold.start.column;
+ }
+
+ this.$keepDesiredColumnOnChange = true;
+ this.lead.setPosition(row, column);
+ this.$keepDesiredColumnOnChange = false;
+
+ if (!keepDesiredColumn)
+ this.$desiredColumn = null;
+ };
+ this.moveCursorToScreen = function(row, column, keepDesiredColumn) {
+ var pos = this.session.screenToDocumentPosition(row, column);
+ this.moveCursorTo(pos.row, pos.column, keepDesiredColumn);
+ };
+
+ // remove listeners from document
+ this.detach = function() {
+ this.lead.detach();
+ this.anchor.detach();
+ this.session = this.doc = null;
+ }
+
+ this.fromOrientedRange = function(range) {
+ this.setSelectionRange(range, range.cursor == range.start);
+ this.$desiredColumn = range.desiredColumn || this.$desiredColumn;
+ }
+
+ this.toOrientedRange = function(range) {
+ var r = this.getRange();
+ if (range) {
+ range.start.column = r.start.column;
+ range.start.row = r.start.row;
+ range.end.column = r.end.column;
+ range.end.row = r.end.row;
+ } else {
+ range = r;
+ }
+
+ range.cursor = this.isBackwards() ? range.start : range.end;
+ range.desiredColumn = this.$desiredColumn;
+ return range;
+ }
+
+}).call(Selection.prototype);
+
+exports.Selection = Selection;
+});
+
+ace.define('ace/range', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+/**
+ * class Range
+ *
+ * This object is used in various places to indicate a region within the editor. To better visualize how this works, imagine a rectangle. Each quadrant of the rectangle is analogus to a range, as ranges contain a starting row and starting column, and an ending row, and ending column.
+ *
+ **/
+
+/**
+ * new Range(startRow, startColumn, endRow, endColumn)
+ * - startRow (Number): The starting row
+ * - startColumn (Number): The starting column
+ * - endRow (Number): The ending row
+ * - endColumn (Number): The ending column
+ *
+ * Creates a new `Range` object with the given starting and ending row and column points.
+ *
+ **/
+var Range = function(startRow, startColumn, endRow, endColumn) {
+ this.start = {
+ row: startRow,
+ column: startColumn
+ };
+
+ this.end = {
+ row: endRow,
+ column: endColumn
+ };
+};
+
+(function() {
+ /**
+ * Range.isEqual(range) -> Boolean
+ * - range (Range): A range to check against
+ *
+ * Returns `true` if and only if the starting row and column, and ending tow and column, are equivalent to those given by `range`.
+ *
+ **/
+ this.isEqual = function(range) {
+ return this.start.row == range.start.row &&
+ this.end.row == range.end.row &&
+ this.start.column == range.start.column &&
+ this.end.column == range.end.column
+ };
+ this.toString = function() {
+ return ("Range: [" + this.start.row + "/" + this.start.column +
+ "] -> [" + this.end.row + "/" + this.end.column + "]");
+ };
+
+ this.contains = function(row, column) {
+ return this.compare(row, column) == 0;
+ };
+ this.compareRange = function(range) {
+ var cmp,
+ end = range.end,
+ start = range.start;
+
+ cmp = this.compare(end.row, end.column);
+ if (cmp == 1) {
+ cmp = this.compare(start.row, start.column);
+ if (cmp == 1) {
+ return 2;
+ } else if (cmp == 0) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else if (cmp == -1) {
+ return -2;
+ } else {
+ cmp = this.compare(start.row, start.column);
+ if (cmp == -1) {
+ return -1;
+ } else if (cmp == 1) {
+ return 42;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ /** related to: Range.compare
+ * Range.comparePoint(p) -> Number
+ * - p (Range): A point to compare with
+ * + (Number): This method returns one of the following numbers:
+ * * `0` if the two points are exactly equal
+ * * `-1` if `p.row` is less then the calling range
+ * * `1` if `p.row` is greater than the calling range
+ *
+ * If the starting row of the calling range is equal to `p.row`, and:
+ * * `p.column` is greater than or equal to the calling range's starting column, this returns `0`
+ * * Otherwise, it returns -1
+ *
+ * If the ending row of the calling range is equal to `p.row`, and:
+ * * `p.column` is less than or equal to the calling range's ending column, this returns `0`
+ * * Otherwise, it returns 1
+ *
+ * Checks the row and column points of `p` with the row and column points of the calling range.
+ *
+ *
+ *
+ **/
+ this.comparePoint = function(p) {
+ return this.compare(p.row, p.column);
+ }
+
+ /** related to: Range.comparePoint
+ * Range.containsRange(range) -> Boolean
+ * - range (Range): A range to compare with
+ *
+ * Checks the start and end points of `range` and compares them to the calling range. Returns `true` if the `range` is contained within the caller's range.
+ *
+ **/
+ this.containsRange = function(range) {
+ return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0;
+ }
+
+ /**
+ * Range.intersects(range) -> Boolean
+ * - range (Range): A range to compare with
+ *
+ * Returns `true` if passed in `range` intersects with the one calling this method.
+ *
+ **/
+ this.intersects = function(range) {
+ var cmp = this.compareRange(range);
+ return (cmp == -1 || cmp == 0 || cmp == 1);
+ }
+
+ /**
+ * Range.isEnd(row, column) -> Boolean
+ * - row (Number): A row point to compare with
+ * - column (Number): A column point to compare with
+ *
+ * Returns `true` if the caller's ending row point is the same as `row`, and if the caller's ending column is the same as `column`.
+ *
+ **/
+ this.isEnd = function(row, column) {
+ return this.end.row == row && this.end.column == column;
+ }
+
+ /**
+ * Range.isStart(row, column) -> Boolean
+ * - row (Number): A row point to compare with
+ * - column (Number): A column point to compare with
+ *
+ * Returns `true` if the caller's starting row point is the same as `row`, and if the caller's starting column is the same as `column`.
+ *
+ **/
+ this.isStart = function(row, column) {
+ return this.start.row == row && this.start.column == column;
+ }
+
+ /**
+ * Range.setStart(row, column)
+ * - row (Number): A row point to set
+ * - column (Number): A column point to set
+ *
+ * Sets the starting row and column for the range.
+ *
+ **/
+ this.setStart = function(row, column) {
+ if (typeof row == "object") {
+ this.start.column = row.column;
+ this.start.row = row.row;
+ } else {
+ this.start.row = row;
+ this.start.column = column;
+ }
+ }
+
+ /**
+ * Range.setEnd(row, column)
+ * - row (Number): A row point to set
+ * - column (Number): A column point to set
+ *
+ * Sets the starting row and column for the range.
+ *
+ **/
+ this.setEnd = function(row, column) {
+ if (typeof row == "object") {
+ this.end.column = row.column;
+ this.end.row = row.row;
+ } else {
+ this.end.row = row;
+ this.end.column = column;
+ }
+ }
+
+ /** related to: Range.compare
+ * Range.inside(row, column) -> Boolean
+ * - row (Number): A row point to compare with
+ * - column (Number): A column point to compare with
+ *
+ * Returns `true` if the `row` and `column` are within the given range.
+ *
+ **/
+ this.inside = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isEnd(row, column) || this.isStart(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** related to: Range.compare
+ * Range.insideStart(row, column) -> Boolean
+ * - row (Number): A row point to compare with
+ * - column (Number): A column point to compare with
+ *
+ * Returns `true` if the `row` and `column` are within the given range's starting points.
+ *
+ **/
+ this.insideStart = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isEnd(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** related to: Range.compare
+ * Range.insideEnd(row, column) -> Boolean
+ * - row (Number): A row point to compare with
+ * - column (Number): A column point to compare with
+ *
+ * Returns `true` if the `row` and `column` are within the given range's ending points.
+ *
+ **/
+ this.insideEnd = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isStart(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Range.compare(row, column) -> Number
+ * - row (Number): A row point to compare with
+ * - column (Number): A column point to compare with
+ * + (Number): This method returns one of the following numbers:
+ * * `0` if the two points are exactly equal
+ * * `-1` if `p.row` is less then the calling range
+ * * `1` if `p.row` is greater than the calling range
+ *
+ * If the starting row of the calling range is equal to `p.row`, and:
+ * * `p.column` is greater than or equal to the calling range's starting column, this returns `0`
+ * * Otherwise, it returns -1
+ *
+ * If the ending row of the calling range is equal to `p.row`, and:
+ * * `p.column` is less than or equal to the calling range's ending column, this returns `0`
+ * * Otherwise, it returns 1
+ *
+ * Checks the row and column points with the row and column points of the calling range.
+ *
+ *
+ **/
+ this.compare = function(row, column) {
+ if (!this.isMultiLine()) {
+ if (row === this.start.row) {
+ return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0);
+ };
+ }
+
+ if (row < this.start.row)
+ return -1;
+
+ if (row > this.end.row)
+ return 1;
+
+ if (this.start.row === row)
+ return column >= this.start.column ? 0 : -1;
+
+ if (this.end.row === row)
+ return column <= this.end.column ? 0 : 1;
+
+ return 0;
+ };
+ this.compareStart = function(row, column) {
+ if (this.start.row == row && this.start.column == column) {
+ return -1;
+ } else {
+ return this.compare(row, column);
+ }
+ }
+
+ /**
+ * Range.compareEnd(row, column) -> Number
+ * - row (Number): A row point to compare with
+ * - column (Number): A column point to compare with
+ * + (Number): This method returns one of the following numbers:
+ * * `0` if the two points are exactly equal
+ * * `-1` if `p.row` is less then the calling range
+ * * `1` if `p.row` is greater than the calling range, or if `isEnd` is `true.
+ *
+ * If the starting row of the calling range is equal to `p.row`, and:
+ * * `p.column` is greater than or equal to the calling range's starting column, this returns `0`
+ * * Otherwise, it returns -1
+ *
+ * If the ending row of the calling range is equal to `p.row`, and:
+ * * `p.column` is less than or equal to the calling range's ending column, this returns `0`
+ * * Otherwise, it returns 1
+ *
+ * Checks the row and column points with the row and column points of the calling range.
+ *
+ *
+ **/
+ this.compareEnd = function(row, column) {
+ if (this.end.row == row && this.end.column == column) {
+ return 1;
+ } else {
+ return this.compare(row, column);
+ }
+ }
+
+ /**
+ * Range.compareInside(row, column) -> Number
+ * - row (Number): A row point to compare with
+ * - column (Number): A column point to compare with
+ * + (Number): This method returns one of the following numbers:
+ * * `1` if the ending row of the calling range is equal to `row`, and the ending column of the calling range is equal to `column`
+ * * `-1` if the starting row of the calling range is equal to `row`, and the starting column of the calling range is equal to `column`
+ *
+ * Otherwise, it returns the value after calling [[Range.compare `compare()`]].
+ *
+ * Checks the row and column points with the row and column points of the calling range.
+ *
+ *
+ *
+ **/
+ this.compareInside = function(row, column) {
+ if (this.end.row == row && this.end.column == column) {
+ return 1;
+ } else if (this.start.row == row && this.start.column == column) {
+ return -1;
+ } else {
+ return this.compare(row, column);
+ }
+ }
+
+ /**
+ * Range.clipRows(firstRow, lastRow) -> Range
+ * - firstRow (Number): The starting row
+ * - lastRow (Number): The ending row
+ *
+ * Returns the part of the current `Range` that occurs within the boundaries of `firstRow` and `lastRow` as a new `Range` object.
+ *
+ **/
+ this.clipRows = function(firstRow, lastRow) {
+ if (this.end.row > lastRow) {
+ var end = {
+ row: lastRow+1,
+ column: 0
+ };
+ }
+
+ if (this.start.row > lastRow) {
+ var start = {
+ row: lastRow+1,
+ column: 0
+ };
+ }
+
+ if (this.start.row < firstRow) {
+ var start = {
+ row: firstRow,
+ column: 0
+ };
+ }
+
+ if (this.end.row < firstRow) {
+ var end = {
+ row: firstRow,
+ column: 0
+ };
+ }
+ return Range.fromPoints(start || this.start, end || this.end);
+ };
+ this.extend = function(row, column) {
+ var cmp = this.compare(row, column);
+
+ if (cmp == 0)
+ return this;
+ else if (cmp == -1)
+ var start = {row: row, column: column};
+ else
+ var end = {row: row, column: column};
+
+ return Range.fromPoints(start || this.start, end || this.end);
+ };
+
+ this.isEmpty = function() {
+ return (this.start.row == this.end.row && this.start.column == this.end.column);
+ };
+ this.isMultiLine = function() {
+ return (this.start.row !== this.end.row);
+ };
+ this.clone = function() {
+ return Range.fromPoints(this.start, this.end);
+ };
+ this.collapseRows = function() {
+ if (this.end.column == 0)
+ return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0)
+ else
+ return new Range(this.start.row, 0, this.end.row, 0)
+ };
+ this.toScreenRange = function(session) {
+ var screenPosStart =
+ session.documentToScreenPosition(this.start);
+ var screenPosEnd =
+ session.documentToScreenPosition(this.end);
+
+ return new Range(
+ screenPosStart.row, screenPosStart.column,
+ screenPosEnd.row, screenPosEnd.column
+ );
+ };
+
+}).call(Range.prototype);
+Range.fromPoints = function(start, end) {
+ return new Range(start.row, start.column, end.row, end.column);
+};
+
+exports.Range = Range;
+});
+
+ace.define('ace/mode/text', ['require', 'exports', 'module' , 'ace/tokenizer', 'ace/mode/text_highlight_rules', 'ace/mode/behaviour', 'ace/unicode'], function(require, exports, module) {
+
+
+var Tokenizer = require("../tokenizer").Tokenizer;
+var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
+var Behaviour = require("./behaviour").Behaviour;
+var unicode = require("../unicode");
+
+var Mode = function() {
+ this.$tokenizer = new Tokenizer(new TextHighlightRules().getRules());
+ this.$behaviour = new Behaviour();
+};
+
+(function() {
+
+ this.tokenRe = new RegExp("^["
+ + unicode.packages.L
+ + unicode.packages.Mn + unicode.packages.Mc
+ + unicode.packages.Nd
+ + unicode.packages.Pc + "\\$_]+", "g"
+ );
+
+ this.nonTokenRe = new RegExp("^(?:[^"
+ + unicode.packages.L
+ + unicode.packages.Mn + unicode.packages.Mc
+ + unicode.packages.Nd
+ + unicode.packages.Pc + "\\$_]|\s])+", "g"
+ );
+
+ this.getTokenizer = function() {
+ return this.$tokenizer;
+ };
+
+ this.toggleCommentLines = function(state, doc, startRow, endRow) {
+ };
+
+ this.getNextLineIndent = function(state, line, tab) {
+ return "";
+ };
+
+ this.checkOutdent = function(state, line, input) {
+ return false;
+ };
+
+ this.autoOutdent = function(state, doc, row) {
+ };
+
+ this.$getIndent = function(line) {
+ var match = line.match(/^(\s+)/);
+ if (match) {
+ return match[1];
+ }
+
+ return "";
+ };
+
+ this.createWorker = function(session) {
+ return null;
+ };
+
+ this.createModeDelegates = function (mapping) {
+ if (!this.$embeds) {
+ return;
+ }
+ this.$modes = {};
+ for (var i = 0; i < this.$embeds.length; i++) {
+ if (mapping[this.$embeds[i]]) {
+ this.$modes[this.$embeds[i]] = new mapping[this.$embeds[i]]();
+ }
+ }
+
+ var delegations = ['toggleCommentLines', 'getNextLineIndent', 'checkOutdent', 'autoOutdent', 'transformAction'];
+
+ for (var i = 0; i < delegations.length; i++) {
+ (function(scope) {
+ var functionName = delegations[i];
+ var defaultHandler = scope[functionName];
+ scope[delegations[i]] = function() {
+ return this.$delegator(functionName, arguments, defaultHandler);
+ }
+ } (this));
+ }
+ }
+
+ this.$delegator = function(method, args, defaultHandler) {
+ var state = args[0];
+
+ for (var i = 0; i < this.$embeds.length; i++) {
+ if (!this.$modes[this.$embeds[i]]) continue;
+
+ var split = state.split(this.$embeds[i]);
+ if (!split[0] && split[1]) {
+ args[0] = split[1];
+ var mode = this.$modes[this.$embeds[i]];
+ return mode[method].apply(mode, args);
+ }
+ }
+ var ret = defaultHandler.apply(this, args);
+ return defaultHandler ? ret : undefined;
+ };
+
+ this.transformAction = function(state, action, editor, session, param) {
+ if (this.$behaviour) {
+ var behaviours = this.$behaviour.getBehaviours();
+ for (var key in behaviours) {
+ if (behaviours[key][action]) {
+ var ret = behaviours[key][action].apply(this, arguments);
+ if (ret) {
+ return ret;
+ }
+ }
+ }
+ }
+ }
+
+}).call(Mode.prototype);
+
+exports.Mode = Mode;
+});
+
+ace.define('ace/tokenizer', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+/**
+ * class Tokenizer
+ *
+ * This class takes a set of highlighting rules, and creates a tokenizer out of them. For more information, see [the wiki on extending highlighters](https://github.com/ajaxorg/ace/wiki/Creating-or-Extending-an-Edit-Mode#wiki-extendingTheHighlighter).
+ *
+ **/
+
+/**
+ * new Tokenizer(rules, flag)
+ * - rules (Object): The highlighting rules
+ * - flag (String): Any additional regular expression flags to pass (like "i" for case insensitive)
+ *
+ * Constructs a new tokenizer based on the given rules and flags.
+ *
+ **/
+var Tokenizer = function(rules, flag) {
+ flag = flag ? "g" + flag : "g";
+ this.rules = rules;
+
+ this.regExps = {};
+ this.matchMappings = {};
+ for ( var key in this.rules) {
+ var rule = this.rules[key];
+ var state = rule;
+ var ruleRegExps = [];
+ var matchTotal = 0;
+ var mapping = this.matchMappings[key] = {};
+
+ for ( var i = 0; i < state.length; i++) {
+
+ if (state[i].regex instanceof RegExp)
+ state[i].regex = state[i].regex.toString().slice(1, -1);
+
+ // Count number of matching groups. 2 extra groups from the full match
+ // And the catch-all on the end (used to force a match);
+ var matchcount = new RegExp("(?:(" + state[i].regex + ")|(.))").exec("a").length - 2;
+
+ // Replace any backreferences and offset appropriately.
+ var adjustedregex = state[i].regex.replace(/\\([0-9]+)/g, function (match, digit) {
+ return "\\" + (parseInt(digit, 10) + matchTotal + 1);
+ });
+
+ if (matchcount > 1 && state[i].token.length !== matchcount-1)
+ throw new Error("For " + state[i].regex + " the matching groups and length of the token array don't match (rule #" + i + " of state " + key + ")");
+
+ mapping[matchTotal] = {
+ rule: i,
+ len: matchcount
+ };
+ matchTotal += matchcount;
+
+ ruleRegExps.push(adjustedregex);
+ }
+
+ this.regExps[key] = new RegExp("(?:(" + ruleRegExps.join(")|(") + ")|(.))", flag);
+ }
+};
+
+(function() {
+
+ /**
+ * Tokenizer.getLineTokens() -> Object
+ *
+ * Returns an object containing two properties: `tokens`, which contains all the tokens; and `state`, the current state.
+ **/
+ this.getLineTokens = function(line, startState) {
+ var currentState = startState || "start";
+ var state = this.rules[currentState];
+ var mapping = this.matchMappings[currentState];
+ var re = this.regExps[currentState];
+ re.lastIndex = 0;
+
+ var match, tokens = [];
+
+ var lastIndex = 0;
+
+ var token = {
+ type: null,
+ value: ""
+ };
+
+ while (match = re.exec(line)) {
+ var type = "text";
+ var rule = null;
+ var value = [match[0]];
+
+ for (var i = 0; i < match.length-2; i++) {
+ if (match[i + 1] === undefined)
+ continue;
+
+ rule = state[mapping[i].rule];
+
+ if (mapping[i].len > 1)
+ value = match.slice(i+2, i+1+mapping[i].len);
+
+ // compute token type
+ if (typeof rule.token == "function")
+ type = rule.token.apply(this, value);
+ else
+ type = rule.token;
+
+ if (rule.next) {
+ currentState = rule.next;
+ state = this.rules[currentState];
+ mapping = this.matchMappings[currentState];
+ lastIndex = re.lastIndex;
+
+ re = this.regExps[currentState];
+
+ if (re === undefined) {
+ throw new Error("You indicated a state of " + rule.next + " to go to, but it doesn't exist!");
+ }
+
+ re.lastIndex = lastIndex;
+ }
+ break;
+ }
+
+ if (value[0]) {
+ if (typeof type == "string") {
+ value = [value.join("")];
+ type = [type];
+ }
+ for (var i = 0; i < value.length; i++) {
+ if (!value[i])
+ continue;
+
+ if ((!rule || rule.merge || type[i] === "text") && token.type === type[i]) {
+ token.value += value[i];
+ } else {
+ if (token.type)
+ tokens.push(token);
+
+ token = {
+ type: type[i],
+ value: value[i]
+ };
+ }
+ }
+ }
+
+ if (lastIndex == line.length)
+ break;
+
+ lastIndex = re.lastIndex;
+ }
+
+ if (token.type)
+ tokens.push(token);
+
+ return {
+ tokens : tokens,
+ state : currentState
+ };
+ };
+
+}).call(Tokenizer.prototype);
+
+exports.Tokenizer = Tokenizer;
+});
+
+ace.define('ace/mode/text_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/lang'], function(require, exports, module) {
+
+
+var lang = require("../lib/lang");
+
+var TextHighlightRules = function() {
+
+ // regexp must not have capturing parentheses
+ // regexps are ordered -> the first match is used
+
+ this.$rules = {
+ "start" : [{
+ token : "empty_line",
+ regex : '^$'
+ }, {
+ token : "text",
+ regex : ".+"
+ }]
+ };
+};
+
+(function() {
+
+ this.addRules = function(rules, prefix) {
+ for (var key in rules) {
+ var state = rules[key];
+ for (var i=0; i
+Uses the Unicode 5.2 character database
+
+This package for the XRegExp Unicode plugin enables the following Unicode categories (aka properties):
+
+L - Letter (the top-level Letter category is included in the Unicode plugin base script)
+ Ll - Lowercase letter
+ Lu - Uppercase letter
+ Lt - Titlecase letter
+ Lm - Modifier letter
+ Lo - Letter without case
+M - Mark
+ Mn - Non-spacing mark
+ Mc - Spacing combining mark
+ Me - Enclosing mark
+N - Number
+ Nd - Decimal digit
+ Nl - Letter number
+ No - Other number
+P - Punctuation
+ Pd - Dash punctuation
+ Ps - Open punctuation
+ Pe - Close punctuation
+ Pi - Initial punctuation
+ Pf - Final punctuation
+ Pc - Connector punctuation
+ Po - Other punctuation
+S - Symbol
+ Sm - Math symbol
+ Sc - Currency symbol
+ Sk - Modifier symbol
+ So - Other symbol
+Z - Separator
+ Zs - Space separator
+ Zl - Line separator
+ Zp - Paragraph separator
+C - Other
+ Cc - Control
+ Cf - Format
+ Co - Private use
+ Cs - Surrogate
+ Cn - Unassigned
+
+Example usage:
+
+ \p{N}
+ \p{Cn}
+*/
+
+
+// will be populated by addUnicodePackage
+exports.packages = {};
+
+addUnicodePackage({
+ L: "0041-005A0061-007A00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05250531-055605590561-058705D0-05EA05F0-05F20621-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280904-0939093D09500958-0961097109720979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10D05-0D0C0D0E-0D100D12-0D280D2A-0D390D3D0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC0EDD0F000F40-0F470F49-0F6C0F88-0F8B1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510D0-10FA10FC1100-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209421022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2D00-2D252D30-2D652D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31B731F0-31FF3400-4DB54E00-9FCBA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A65FA662-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78BA78CA7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA2DFA30-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC",
+ Ll: "0061-007A00AA00B500BA00DF-00F600F8-00FF01010103010501070109010B010D010F01110113011501170119011B011D011F01210123012501270129012B012D012F01310133013501370138013A013C013E014001420144014601480149014B014D014F01510153015501570159015B015D015F01610163016501670169016B016D016F0171017301750177017A017C017E-0180018301850188018C018D019201950199-019B019E01A101A301A501A801AA01AB01AD01B001B401B601B901BA01BD-01BF01C601C901CC01CE01D001D201D401D601D801DA01DC01DD01DF01E101E301E501E701E901EB01ED01EF01F001F301F501F901FB01FD01FF02010203020502070209020B020D020F02110213021502170219021B021D021F02210223022502270229022B022D022F02310233-0239023C023F0240024202470249024B024D024F-02930295-02AF037103730377037B-037D039003AC-03CE03D003D103D5-03D703D903DB03DD03DF03E103E303E503E703E903EB03ED03EF-03F303F503F803FB03FC0430-045F04610463046504670469046B046D046F04710473047504770479047B047D047F0481048B048D048F04910493049504970499049B049D049F04A104A304A504A704A904AB04AD04AF04B104B304B504B704B904BB04BD04BF04C204C404C604C804CA04CC04CE04CF04D104D304D504D704D904DB04DD04DF04E104E304E504E704E904EB04ED04EF04F104F304F504F704F904FB04FD04FF05010503050505070509050B050D050F05110513051505170519051B051D051F0521052305250561-05871D00-1D2B1D62-1D771D79-1D9A1E011E031E051E071E091E0B1E0D1E0F1E111E131E151E171E191E1B1E1D1E1F1E211E231E251E271E291E2B1E2D1E2F1E311E331E351E371E391E3B1E3D1E3F1E411E431E451E471E491E4B1E4D1E4F1E511E531E551E571E591E5B1E5D1E5F1E611E631E651E671E691E6B1E6D1E6F1E711E731E751E771E791E7B1E7D1E7F1E811E831E851E871E891E8B1E8D1E8F1E911E931E95-1E9D1E9F1EA11EA31EA51EA71EA91EAB1EAD1EAF1EB11EB31EB51EB71EB91EBB1EBD1EBF1EC11EC31EC51EC71EC91ECB1ECD1ECF1ED11ED31ED51ED71ED91EDB1EDD1EDF1EE11EE31EE51EE71EE91EEB1EED1EEF1EF11EF31EF51EF71EF91EFB1EFD1EFF-1F071F10-1F151F20-1F271F30-1F371F40-1F451F50-1F571F60-1F671F70-1F7D1F80-1F871F90-1F971FA0-1FA71FB0-1FB41FB61FB71FBE1FC2-1FC41FC61FC71FD0-1FD31FD61FD71FE0-1FE71FF2-1FF41FF61FF7210A210E210F2113212F21342139213C213D2146-2149214E21842C30-2C5E2C612C652C662C682C6A2C6C2C712C732C742C76-2C7C2C812C832C852C872C892C8B2C8D2C8F2C912C932C952C972C992C9B2C9D2C9F2CA12CA32CA52CA72CA92CAB2CAD2CAF2CB12CB32CB52CB72CB92CBB2CBD2CBF2CC12CC32CC52CC72CC92CCB2CCD2CCF2CD12CD32CD52CD72CD92CDB2CDD2CDF2CE12CE32CE42CEC2CEE2D00-2D25A641A643A645A647A649A64BA64DA64FA651A653A655A657A659A65BA65DA65FA663A665A667A669A66BA66DA681A683A685A687A689A68BA68DA68FA691A693A695A697A723A725A727A729A72BA72DA72F-A731A733A735A737A739A73BA73DA73FA741A743A745A747A749A74BA74DA74FA751A753A755A757A759A75BA75DA75FA761A763A765A767A769A76BA76DA76FA771-A778A77AA77CA77FA781A783A785A787A78CFB00-FB06FB13-FB17FF41-FF5A",
+ Lu: "0041-005A00C0-00D600D8-00DE01000102010401060108010A010C010E01100112011401160118011A011C011E01200122012401260128012A012C012E01300132013401360139013B013D013F0141014301450147014A014C014E01500152015401560158015A015C015E01600162016401660168016A016C016E017001720174017601780179017B017D018101820184018601870189-018B018E-0191019301940196-0198019C019D019F01A001A201A401A601A701A901AC01AE01AF01B1-01B301B501B701B801BC01C401C701CA01CD01CF01D101D301D501D701D901DB01DE01E001E201E401E601E801EA01EC01EE01F101F401F6-01F801FA01FC01FE02000202020402060208020A020C020E02100212021402160218021A021C021E02200222022402260228022A022C022E02300232023A023B023D023E02410243-02460248024A024C024E03700372037603860388-038A038C038E038F0391-03A103A3-03AB03CF03D2-03D403D803DA03DC03DE03E003E203E403E603E803EA03EC03EE03F403F703F903FA03FD-042F04600462046404660468046A046C046E04700472047404760478047A047C047E0480048A048C048E04900492049404960498049A049C049E04A004A204A404A604A804AA04AC04AE04B004B204B404B604B804BA04BC04BE04C004C104C304C504C704C904CB04CD04D004D204D404D604D804DA04DC04DE04E004E204E404E604E804EA04EC04EE04F004F204F404F604F804FA04FC04FE05000502050405060508050A050C050E05100512051405160518051A051C051E0520052205240531-055610A0-10C51E001E021E041E061E081E0A1E0C1E0E1E101E121E141E161E181E1A1E1C1E1E1E201E221E241E261E281E2A1E2C1E2E1E301E321E341E361E381E3A1E3C1E3E1E401E421E441E461E481E4A1E4C1E4E1E501E521E541E561E581E5A1E5C1E5E1E601E621E641E661E681E6A1E6C1E6E1E701E721E741E761E781E7A1E7C1E7E1E801E821E841E861E881E8A1E8C1E8E1E901E921E941E9E1EA01EA21EA41EA61EA81EAA1EAC1EAE1EB01EB21EB41EB61EB81EBA1EBC1EBE1EC01EC21EC41EC61EC81ECA1ECC1ECE1ED01ED21ED41ED61ED81EDA1EDC1EDE1EE01EE21EE41EE61EE81EEA1EEC1EEE1EF01EF21EF41EF61EF81EFA1EFC1EFE1F08-1F0F1F18-1F1D1F28-1F2F1F38-1F3F1F48-1F4D1F591F5B1F5D1F5F1F68-1F6F1FB8-1FBB1FC8-1FCB1FD8-1FDB1FE8-1FEC1FF8-1FFB21022107210B-210D2110-211221152119-211D212421262128212A-212D2130-2133213E213F214521832C00-2C2E2C602C62-2C642C672C692C6B2C6D-2C702C722C752C7E-2C802C822C842C862C882C8A2C8C2C8E2C902C922C942C962C982C9A2C9C2C9E2CA02CA22CA42CA62CA82CAA2CAC2CAE2CB02CB22CB42CB62CB82CBA2CBC2CBE2CC02CC22CC42CC62CC82CCA2CCC2CCE2CD02CD22CD42CD62CD82CDA2CDC2CDE2CE02CE22CEB2CEDA640A642A644A646A648A64AA64CA64EA650A652A654A656A658A65AA65CA65EA662A664A666A668A66AA66CA680A682A684A686A688A68AA68CA68EA690A692A694A696A722A724A726A728A72AA72CA72EA732A734A736A738A73AA73CA73EA740A742A744A746A748A74AA74CA74EA750A752A754A756A758A75AA75CA75EA760A762A764A766A768A76AA76CA76EA779A77BA77DA77EA780A782A784A786A78BFF21-FF3A",
+ Lt: "01C501C801CB01F21F88-1F8F1F98-1F9F1FA8-1FAF1FBC1FCC1FFC",
+ Lm: "02B0-02C102C6-02D102E0-02E402EC02EE0374037A0559064006E506E607F407F507FA081A0824082809710E460EC610FC17D718431AA71C78-1C7D1D2C-1D611D781D9B-1DBF2071207F2090-20942C7D2D6F2E2F30053031-3035303B309D309E30FC-30FEA015A4F8-A4FDA60CA67FA717-A71FA770A788A9CFAA70AADDFF70FF9EFF9F",
+ Lo: "01BB01C0-01C3029405D0-05EA05F0-05F20621-063F0641-064A066E066F0671-06D306D506EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA0800-08150904-0939093D09500958-096109720979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10D05-0D0C0D0E-0D100D12-0D280D2A-0D390D3D0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E450E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EDC0EDD0F000F40-0F470F49-0F6C0F88-0F8B1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10D0-10FA1100-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317DC1820-18421844-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541B05-1B331B45-1B4B1B83-1BA01BAE1BAF1C00-1C231C4D-1C4F1C5A-1C771CE9-1CEC1CEE-1CF12135-21382D30-2D652D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE3006303C3041-3096309F30A1-30FA30FF3105-312D3131-318E31A0-31B731F0-31FF3400-4DB54E00-9FCBA000-A014A016-A48CA4D0-A4F7A500-A60BA610-A61FA62AA62BA66EA6A0-A6E5A7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2AA00-AA28AA40-AA42AA44-AA4BAA60-AA6FAA71-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADBAADCABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA2DFA30-FA6DFA70-FAD9FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF66-FF6FFF71-FF9DFFA0-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC",
+ M: "0300-036F0483-04890591-05BD05BF05C105C205C405C505C70610-061A064B-065E067006D6-06DC06DE-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0900-0903093C093E-094E0951-0955096209630981-098309BC09BE-09C409C709C809CB-09CD09D709E209E30A01-0A030A3C0A3E-0A420A470A480A4B-0A4D0A510A700A710A750A81-0A830ABC0ABE-0AC50AC7-0AC90ACB-0ACD0AE20AE30B01-0B030B3C0B3E-0B440B470B480B4B-0B4D0B560B570B620B630B820BBE-0BC20BC6-0BC80BCA-0BCD0BD70C01-0C030C3E-0C440C46-0C480C4A-0C4D0C550C560C620C630C820C830CBC0CBE-0CC40CC6-0CC80CCA-0CCD0CD50CD60CE20CE30D020D030D3E-0D440D46-0D480D4A-0D4D0D570D620D630D820D830DCA0DCF-0DD40DD60DD8-0DDF0DF20DF30E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F3E0F3F0F71-0F840F860F870F90-0F970F99-0FBC0FC6102B-103E1056-1059105E-10601062-10641067-106D1071-10741082-108D108F109A-109D135F1712-17141732-1734175217531772177317B6-17D317DD180B-180D18A91920-192B1930-193B19B0-19C019C819C91A17-1A1B1A55-1A5E1A60-1A7C1A7F1B00-1B041B34-1B441B6B-1B731B80-1B821BA1-1BAA1C24-1C371CD0-1CD21CD4-1CE81CED1CF21DC0-1DE61DFD-1DFF20D0-20F02CEF-2CF12DE0-2DFF302A-302F3099309AA66F-A672A67CA67DA6F0A6F1A802A806A80BA823-A827A880A881A8B4-A8C4A8E0-A8F1A926-A92DA947-A953A980-A983A9B3-A9C0AA29-AA36AA43AA4CAA4DAA7BAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1ABE3-ABEAABECABEDFB1EFE00-FE0FFE20-FE26",
+ Mn: "0300-036F0483-04870591-05BD05BF05C105C205C405C505C70610-061A064B-065E067006D6-06DC06DF-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0900-0902093C0941-0948094D0951-095509620963098109BC09C1-09C409CD09E209E30A010A020A3C0A410A420A470A480A4B-0A4D0A510A700A710A750A810A820ABC0AC1-0AC50AC70AC80ACD0AE20AE30B010B3C0B3F0B41-0B440B4D0B560B620B630B820BC00BCD0C3E-0C400C46-0C480C4A-0C4D0C550C560C620C630CBC0CBF0CC60CCC0CCD0CE20CE30D41-0D440D4D0D620D630DCA0DD2-0DD40DD60E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F71-0F7E0F80-0F840F860F870F90-0F970F99-0FBC0FC6102D-10301032-10371039103A103D103E10581059105E-10601071-1074108210851086108D109D135F1712-17141732-1734175217531772177317B7-17BD17C617C9-17D317DD180B-180D18A91920-19221927192819321939-193B1A171A181A561A58-1A5E1A601A621A65-1A6C1A73-1A7C1A7F1B00-1B031B341B36-1B3A1B3C1B421B6B-1B731B801B811BA2-1BA51BA81BA91C2C-1C331C361C371CD0-1CD21CD4-1CE01CE2-1CE81CED1DC0-1DE61DFD-1DFF20D0-20DC20E120E5-20F02CEF-2CF12DE0-2DFF302A-302F3099309AA66FA67CA67DA6F0A6F1A802A806A80BA825A826A8C4A8E0-A8F1A926-A92DA947-A951A980-A982A9B3A9B6-A9B9A9BCAA29-AA2EAA31AA32AA35AA36AA43AA4CAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1ABE5ABE8ABEDFB1EFE00-FE0FFE20-FE26",
+ Mc: "0903093E-09400949-094C094E0982098309BE-09C009C709C809CB09CC09D70A030A3E-0A400A830ABE-0AC00AC90ACB0ACC0B020B030B3E0B400B470B480B4B0B4C0B570BBE0BBF0BC10BC20BC6-0BC80BCA-0BCC0BD70C01-0C030C41-0C440C820C830CBE0CC0-0CC40CC70CC80CCA0CCB0CD50CD60D020D030D3E-0D400D46-0D480D4A-0D4C0D570D820D830DCF-0DD10DD8-0DDF0DF20DF30F3E0F3F0F7F102B102C10311038103B103C105610571062-10641067-106D108310841087-108C108F109A-109C17B617BE-17C517C717C81923-19261929-192B193019311933-193819B0-19C019C819C91A19-1A1B1A551A571A611A631A641A6D-1A721B041B351B3B1B3D-1B411B431B441B821BA11BA61BA71BAA1C24-1C2B1C341C351CE11CF2A823A824A827A880A881A8B4-A8C3A952A953A983A9B4A9B5A9BAA9BBA9BD-A9C0AA2FAA30AA33AA34AA4DAA7BABE3ABE4ABE6ABE7ABE9ABEAABEC",
+ Me: "0488048906DE20DD-20E020E2-20E4A670-A672",
+ N: "0030-003900B200B300B900BC-00BE0660-066906F0-06F907C0-07C90966-096F09E6-09EF09F4-09F90A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BF20C66-0C6F0C78-0C7E0CE6-0CEF0D66-0D750E50-0E590ED0-0ED90F20-0F331040-10491090-10991369-137C16EE-16F017E0-17E917F0-17F91810-18191946-194F19D0-19DA1A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C5920702074-20792080-20892150-21822185-21892460-249B24EA-24FF2776-27932CFD30073021-30293038-303A3192-31953220-32293251-325F3280-328932B1-32BFA620-A629A6E6-A6EFA830-A835A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19",
+ Nd: "0030-00390660-066906F0-06F907C0-07C90966-096F09E6-09EF0A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BEF0C66-0C6F0CE6-0CEF0D66-0D6F0E50-0E590ED0-0ED90F20-0F291040-10491090-109917E0-17E91810-18191946-194F19D0-19DA1A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C59A620-A629A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19",
+ Nl: "16EE-16F02160-21822185-218830073021-30293038-303AA6E6-A6EF",
+ No: "00B200B300B900BC-00BE09F4-09F90BF0-0BF20C78-0C7E0D70-0D750F2A-0F331369-137C17F0-17F920702074-20792080-20892150-215F21892460-249B24EA-24FF2776-27932CFD3192-31953220-32293251-325F3280-328932B1-32BFA830-A835",
+ P: "0021-00230025-002A002C-002F003A003B003F0040005B-005D005F007B007D00A100AB00B700BB00BF037E0387055A-055F0589058A05BE05C005C305C605F305F40609060A060C060D061B061E061F066A-066D06D40700-070D07F7-07F90830-083E0964096509700DF40E4F0E5A0E5B0F04-0F120F3A-0F3D0F850FD0-0FD4104A-104F10FB1361-13681400166D166E169B169C16EB-16ED1735173617D4-17D617D8-17DA1800-180A1944194519DE19DF1A1E1A1F1AA0-1AA61AA8-1AAD1B5A-1B601C3B-1C3F1C7E1C7F1CD32010-20272030-20432045-20512053-205E207D207E208D208E2329232A2768-277527C527C627E6-27EF2983-299829D8-29DB29FC29FD2CF9-2CFC2CFE2CFF2E00-2E2E2E302E313001-30033008-30113014-301F3030303D30A030FBA4FEA4FFA60D-A60FA673A67EA6F2-A6F7A874-A877A8CEA8CFA8F8-A8FAA92EA92FA95FA9C1-A9CDA9DEA9DFAA5C-AA5FAADEAADFABEBFD3EFD3FFE10-FE19FE30-FE52FE54-FE61FE63FE68FE6AFE6BFF01-FF03FF05-FF0AFF0C-FF0FFF1AFF1BFF1FFF20FF3B-FF3DFF3FFF5BFF5DFF5F-FF65",
+ Pd: "002D058A05BE140018062010-20152E172E1A301C303030A0FE31FE32FE58FE63FF0D",
+ Ps: "0028005B007B0F3A0F3C169B201A201E2045207D208D23292768276A276C276E27702772277427C527E627E827EA27EC27EE2983298529872989298B298D298F299129932995299729D829DA29FC2E222E242E262E283008300A300C300E3010301430163018301A301DFD3EFE17FE35FE37FE39FE3BFE3DFE3FFE41FE43FE47FE59FE5BFE5DFF08FF3BFF5BFF5FFF62",
+ Pe: "0029005D007D0F3B0F3D169C2046207E208E232A2769276B276D276F27712773277527C627E727E927EB27ED27EF298429862988298A298C298E2990299229942996299829D929DB29FD2E232E252E272E293009300B300D300F3011301530173019301B301E301FFD3FFE18FE36FE38FE3AFE3CFE3EFE40FE42FE44FE48FE5AFE5CFE5EFF09FF3DFF5DFF60FF63",
+ Pi: "00AB2018201B201C201F20392E022E042E092E0C2E1C2E20",
+ Pf: "00BB2019201D203A2E032E052E0A2E0D2E1D2E21",
+ Pc: "005F203F20402054FE33FE34FE4D-FE4FFF3F",
+ Po: "0021-00230025-0027002A002C002E002F003A003B003F0040005C00A100B700BF037E0387055A-055F058905C005C305C605F305F40609060A060C060D061B061E061F066A-066D06D40700-070D07F7-07F90830-083E0964096509700DF40E4F0E5A0E5B0F04-0F120F850FD0-0FD4104A-104F10FB1361-1368166D166E16EB-16ED1735173617D4-17D617D8-17DA1800-18051807-180A1944194519DE19DF1A1E1A1F1AA0-1AA61AA8-1AAD1B5A-1B601C3B-1C3F1C7E1C7F1CD3201620172020-20272030-2038203B-203E2041-20432047-205120532055-205E2CF9-2CFC2CFE2CFF2E002E012E06-2E082E0B2E0E-2E162E182E192E1B2E1E2E1F2E2A-2E2E2E302E313001-3003303D30FBA4FEA4FFA60D-A60FA673A67EA6F2-A6F7A874-A877A8CEA8CFA8F8-A8FAA92EA92FA95FA9C1-A9CDA9DEA9DFAA5C-AA5FAADEAADFABEBFE10-FE16FE19FE30FE45FE46FE49-FE4CFE50-FE52FE54-FE57FE5F-FE61FE68FE6AFE6BFF01-FF03FF05-FF07FF0AFF0CFF0EFF0FFF1AFF1BFF1FFF20FF3CFF61FF64FF65",
+ S: "0024002B003C-003E005E0060007C007E00A2-00A900AC00AE-00B100B400B600B800D700F702C2-02C502D2-02DF02E5-02EB02ED02EF-02FF03750384038503F604820606-0608060B060E060F06E906FD06FE07F609F209F309FA09FB0AF10B700BF3-0BFA0C7F0CF10CF20D790E3F0F01-0F030F13-0F170F1A-0F1F0F340F360F380FBE-0FC50FC7-0FCC0FCE0FCF0FD5-0FD8109E109F13601390-139917DB194019E0-19FF1B61-1B6A1B74-1B7C1FBD1FBF-1FC11FCD-1FCF1FDD-1FDF1FED-1FEF1FFD1FFE20442052207A-207C208A-208C20A0-20B8210021012103-21062108210921142116-2118211E-2123212521272129212E213A213B2140-2144214A-214D214F2190-2328232B-23E82400-24262440-244A249C-24E92500-26CD26CF-26E126E326E8-26FF2701-27042706-2709270C-27272729-274B274D274F-27522756-275E2761-276727942798-27AF27B1-27BE27C0-27C427C7-27CA27CC27D0-27E527F0-29822999-29D729DC-29FB29FE-2B4C2B50-2B592CE5-2CEA2E80-2E992E9B-2EF32F00-2FD52FF0-2FFB300430123013302030363037303E303F309B309C319031913196-319F31C0-31E33200-321E322A-32503260-327F328A-32B032C0-32FE3300-33FF4DC0-4DFFA490-A4C6A700-A716A720A721A789A78AA828-A82BA836-A839AA77-AA79FB29FDFCFDFDFE62FE64-FE66FE69FF04FF0BFF1C-FF1EFF3EFF40FF5CFF5EFFE0-FFE6FFE8-FFEEFFFCFFFD",
+ Sm: "002B003C-003E007C007E00AC00B100D700F703F60606-060820442052207A-207C208A-208C2140-2144214B2190-2194219A219B21A021A321A621AE21CE21CF21D221D421F4-22FF2308-230B23202321237C239B-23B323DC-23E125B725C125F8-25FF266F27C0-27C427C7-27CA27CC27D0-27E527F0-27FF2900-29822999-29D729DC-29FB29FE-2AFF2B30-2B442B47-2B4CFB29FE62FE64-FE66FF0BFF1C-FF1EFF5CFF5EFFE2FFE9-FFEC",
+ Sc: "002400A2-00A5060B09F209F309FB0AF10BF90E3F17DB20A0-20B8A838FDFCFE69FF04FFE0FFE1FFE5FFE6",
+ Sk: "005E006000A800AF00B400B802C2-02C502D2-02DF02E5-02EB02ED02EF-02FF0375038403851FBD1FBF-1FC11FCD-1FCF1FDD-1FDF1FED-1FEF1FFD1FFE309B309CA700-A716A720A721A789A78AFF3EFF40FFE3",
+ So: "00A600A700A900AE00B000B60482060E060F06E906FD06FE07F609FA0B700BF3-0BF80BFA0C7F0CF10CF20D790F01-0F030F13-0F170F1A-0F1F0F340F360F380FBE-0FC50FC7-0FCC0FCE0FCF0FD5-0FD8109E109F13601390-1399194019E0-19FF1B61-1B6A1B74-1B7C210021012103-21062108210921142116-2118211E-2123212521272129212E213A213B214A214C214D214F2195-2199219C-219F21A121A221A421A521A7-21AD21AF-21CD21D021D121D321D5-21F32300-2307230C-231F2322-2328232B-237B237D-239A23B4-23DB23E2-23E82400-24262440-244A249C-24E92500-25B625B8-25C025C2-25F72600-266E2670-26CD26CF-26E126E326E8-26FF2701-27042706-2709270C-27272729-274B274D274F-27522756-275E2761-276727942798-27AF27B1-27BE2800-28FF2B00-2B2F2B452B462B50-2B592CE5-2CEA2E80-2E992E9B-2EF32F00-2FD52FF0-2FFB300430123013302030363037303E303F319031913196-319F31C0-31E33200-321E322A-32503260-327F328A-32B032C0-32FE3300-33FF4DC0-4DFFA490-A4C6A828-A82BA836A837A839AA77-AA79FDFDFFE4FFE8FFEDFFEEFFFCFFFD",
+ Z: "002000A01680180E2000-200A20282029202F205F3000",
+ Zs: "002000A01680180E2000-200A202F205F3000",
+ Zl: "2028",
+ Zp: "2029",
+ C: "0000-001F007F-009F00AD03780379037F-0383038B038D03A20526-05300557055805600588058B-059005C8-05CF05EB-05EF05F5-0605061C061D0620065F06DD070E070F074B074C07B2-07BF07FB-07FF082E082F083F-08FF093A093B094F095609570973-097809800984098D098E0991099209A909B109B3-09B509BA09BB09C509C609C909CA09CF-09D609D8-09DB09DE09E409E509FC-0A000A040A0B-0A0E0A110A120A290A310A340A370A3A0A3B0A3D0A43-0A460A490A4A0A4E-0A500A52-0A580A5D0A5F-0A650A76-0A800A840A8E0A920AA90AB10AB40ABA0ABB0AC60ACA0ACE0ACF0AD1-0ADF0AE40AE50AF00AF2-0B000B040B0D0B0E0B110B120B290B310B340B3A0B3B0B450B460B490B4A0B4E-0B550B58-0B5B0B5E0B640B650B72-0B810B840B8B-0B8D0B910B96-0B980B9B0B9D0BA0-0BA20BA5-0BA70BAB-0BAD0BBA-0BBD0BC3-0BC50BC90BCE0BCF0BD1-0BD60BD8-0BE50BFB-0C000C040C0D0C110C290C340C3A-0C3C0C450C490C4E-0C540C570C5A-0C5F0C640C650C70-0C770C800C810C840C8D0C910CA90CB40CBA0CBB0CC50CC90CCE-0CD40CD7-0CDD0CDF0CE40CE50CF00CF3-0D010D040D0D0D110D290D3A-0D3C0D450D490D4E-0D560D58-0D5F0D640D650D76-0D780D800D810D840D97-0D990DB20DBC0DBE0DBF0DC7-0DC90DCB-0DCE0DD50DD70DE0-0DF10DF5-0E000E3B-0E3E0E5C-0E800E830E850E860E890E8B0E8C0E8E-0E930E980EA00EA40EA60EA80EA90EAC0EBA0EBE0EBF0EC50EC70ECE0ECF0EDA0EDB0EDE-0EFF0F480F6D-0F700F8C-0F8F0F980FBD0FCD0FD9-0FFF10C6-10CF10FD-10FF1249124E124F12571259125E125F1289128E128F12B112B612B712BF12C112C612C712D7131113161317135B-135E137D-137F139A-139F13F5-13FF169D-169F16F1-16FF170D1715-171F1737-173F1754-175F176D17711774-177F17B417B517DE17DF17EA-17EF17FA-17FF180F181A-181F1878-187F18AB-18AF18F6-18FF191D-191F192C-192F193C-193F1941-1943196E196F1975-197F19AC-19AF19CA-19CF19DB-19DD1A1C1A1D1A5F1A7D1A7E1A8A-1A8F1A9A-1A9F1AAE-1AFF1B4C-1B4F1B7D-1B7F1BAB-1BAD1BBA-1BFF1C38-1C3A1C4A-1C4C1C80-1CCF1CF3-1CFF1DE7-1DFC1F161F171F1E1F1F1F461F471F4E1F4F1F581F5A1F5C1F5E1F7E1F7F1FB51FC51FD41FD51FDC1FF01FF11FF51FFF200B-200F202A-202E2060-206F20722073208F2095-209F20B9-20CF20F1-20FF218A-218F23E9-23FF2427-243F244B-245F26CE26E226E4-26E727002705270A270B2728274C274E2753-2755275F27602795-279727B027BF27CB27CD-27CF2B4D-2B4F2B5A-2BFF2C2F2C5F2CF2-2CF82D26-2D2F2D66-2D6E2D70-2D7F2D97-2D9F2DA72DAF2DB72DBF2DC72DCF2DD72DDF2E32-2E7F2E9A2EF4-2EFF2FD6-2FEF2FFC-2FFF3040309730983100-3104312E-3130318F31B8-31BF31E4-31EF321F32FF4DB6-4DBF9FCC-9FFFA48D-A48FA4C7-A4CFA62C-A63FA660A661A674-A67BA698-A69FA6F8-A6FFA78D-A7FAA82C-A82FA83A-A83FA878-A87FA8C5-A8CDA8DA-A8DFA8FC-A8FFA954-A95EA97D-A97FA9CEA9DA-A9DDA9E0-A9FFAA37-AA3FAA4EAA4FAA5AAA5BAA7C-AA7FAAC3-AADAAAE0-ABBFABEEABEFABFA-ABFFD7A4-D7AFD7C7-D7CAD7FC-F8FFFA2EFA2FFA6EFA6FFADA-FAFFFB07-FB12FB18-FB1CFB37FB3DFB3FFB42FB45FBB2-FBD2FD40-FD4FFD90FD91FDC8-FDEFFDFEFDFFFE1A-FE1FFE27-FE2FFE53FE67FE6C-FE6FFE75FEFD-FF00FFBF-FFC1FFC8FFC9FFD0FFD1FFD8FFD9FFDD-FFDFFFE7FFEF-FFFBFFFEFFFF",
+ Cc: "0000-001F007F-009F",
+ Cf: "00AD0600-060306DD070F17B417B5200B-200F202A-202E2060-2064206A-206FFEFFFFF9-FFFB",
+ Co: "E000-F8FF",
+ Cs: "D800-DFFF",
+ Cn: "03780379037F-0383038B038D03A20526-05300557055805600588058B-059005C8-05CF05EB-05EF05F5-05FF06040605061C061D0620065F070E074B074C07B2-07BF07FB-07FF082E082F083F-08FF093A093B094F095609570973-097809800984098D098E0991099209A909B109B3-09B509BA09BB09C509C609C909CA09CF-09D609D8-09DB09DE09E409E509FC-0A000A040A0B-0A0E0A110A120A290A310A340A370A3A0A3B0A3D0A43-0A460A490A4A0A4E-0A500A52-0A580A5D0A5F-0A650A76-0A800A840A8E0A920AA90AB10AB40ABA0ABB0AC60ACA0ACE0ACF0AD1-0ADF0AE40AE50AF00AF2-0B000B040B0D0B0E0B110B120B290B310B340B3A0B3B0B450B460B490B4A0B4E-0B550B58-0B5B0B5E0B640B650B72-0B810B840B8B-0B8D0B910B96-0B980B9B0B9D0BA0-0BA20BA5-0BA70BAB-0BAD0BBA-0BBD0BC3-0BC50BC90BCE0BCF0BD1-0BD60BD8-0BE50BFB-0C000C040C0D0C110C290C340C3A-0C3C0C450C490C4E-0C540C570C5A-0C5F0C640C650C70-0C770C800C810C840C8D0C910CA90CB40CBA0CBB0CC50CC90CCE-0CD40CD7-0CDD0CDF0CE40CE50CF00CF3-0D010D040D0D0D110D290D3A-0D3C0D450D490D4E-0D560D58-0D5F0D640D650D76-0D780D800D810D840D97-0D990DB20DBC0DBE0DBF0DC7-0DC90DCB-0DCE0DD50DD70DE0-0DF10DF5-0E000E3B-0E3E0E5C-0E800E830E850E860E890E8B0E8C0E8E-0E930E980EA00EA40EA60EA80EA90EAC0EBA0EBE0EBF0EC50EC70ECE0ECF0EDA0EDB0EDE-0EFF0F480F6D-0F700F8C-0F8F0F980FBD0FCD0FD9-0FFF10C6-10CF10FD-10FF1249124E124F12571259125E125F1289128E128F12B112B612B712BF12C112C612C712D7131113161317135B-135E137D-137F139A-139F13F5-13FF169D-169F16F1-16FF170D1715-171F1737-173F1754-175F176D17711774-177F17DE17DF17EA-17EF17FA-17FF180F181A-181F1878-187F18AB-18AF18F6-18FF191D-191F192C-192F193C-193F1941-1943196E196F1975-197F19AC-19AF19CA-19CF19DB-19DD1A1C1A1D1A5F1A7D1A7E1A8A-1A8F1A9A-1A9F1AAE-1AFF1B4C-1B4F1B7D-1B7F1BAB-1BAD1BBA-1BFF1C38-1C3A1C4A-1C4C1C80-1CCF1CF3-1CFF1DE7-1DFC1F161F171F1E1F1F1F461F471F4E1F4F1F581F5A1F5C1F5E1F7E1F7F1FB51FC51FD41FD51FDC1FF01FF11FF51FFF2065-206920722073208F2095-209F20B9-20CF20F1-20FF218A-218F23E9-23FF2427-243F244B-245F26CE26E226E4-26E727002705270A270B2728274C274E2753-2755275F27602795-279727B027BF27CB27CD-27CF2B4D-2B4F2B5A-2BFF2C2F2C5F2CF2-2CF82D26-2D2F2D66-2D6E2D70-2D7F2D97-2D9F2DA72DAF2DB72DBF2DC72DCF2DD72DDF2E32-2E7F2E9A2EF4-2EFF2FD6-2FEF2FFC-2FFF3040309730983100-3104312E-3130318F31B8-31BF31E4-31EF321F32FF4DB6-4DBF9FCC-9FFFA48D-A48FA4C7-A4CFA62C-A63FA660A661A674-A67BA698-A69FA6F8-A6FFA78D-A7FAA82C-A82FA83A-A83FA878-A87FA8C5-A8CDA8DA-A8DFA8FC-A8FFA954-A95EA97D-A97FA9CEA9DA-A9DDA9E0-A9FFAA37-AA3FAA4EAA4FAA5AAA5BAA7C-AA7FAAC3-AADAAAE0-ABBFABEEABEFABFA-ABFFD7A4-D7AFD7C7-D7CAD7FC-D7FFFA2EFA2FFA6EFA6FFADA-FAFFFB07-FB12FB18-FB1CFB37FB3DFB3FFB42FB45FBB2-FBD2FD40-FD4FFD90FD91FDC8-FDEFFDFEFDFFFE1A-FE1FFE27-FE2FFE53FE67FE6C-FE6FFE75FEFDFEFEFF00FFBF-FFC1FFC8FFC9FFD0FFD1FFD8FFD9FFDD-FFDFFFE7FFEF-FFF8FFFEFFFF"
+});
+
+function addUnicodePackage (pack) {
+ var codePoint = /\w{4}/g;
+ for (var name in pack)
+ exports.packages[name] = pack[name].replace(codePoint, "\\u$&");
+};
+
+});
+
+ace.define('ace/document', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/event_emitter', 'ace/range', 'ace/anchor'], function(require, exports, module) {
+
+
+var oop = require("./lib/oop");
+var EventEmitter = require("./lib/event_emitter").EventEmitter;
+var Range = require("./range").Range;
+var Anchor = require("./anchor").Anchor;
+
+ /**
+ * new Document([text])
+ * - text (String | Array): The starting text
+ *
+ * Creates a new `Document`. If `text` is included, the `Document` contains those strings; otherwise, it's empty.
+ *
+ **/
+
+var Document = function(text) {
+ this.$lines = [];
+
+ // There has to be one line at least in the document. If you pass an empty
+ // string to the insert function, nothing will happen. Workaround.
+ if (text.length == 0) {
+ this.$lines = [""];
+ } else if (Array.isArray(text)) {
+ this.insertLines(0, text);
+ } else {
+ this.insert({row: 0, column:0}, text);
+ }
+};
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+ this.setValue = function(text) {
+ var len = this.getLength();
+ this.remove(new Range(0, 0, len, this.getLine(len-1).length));
+ this.insert({row: 0, column:0}, text);
+ };
+ this.getValue = function() {
+ return this.getAllLines().join(this.getNewLineCharacter());
+ };
+ this.createAnchor = function(row, column) {
+ return new Anchor(this, row, column);
+ };
+
+ // check for IE split bug
+ if ("aaa".split(/a/).length == 0)
+ this.$split = function(text) {
+ return text.replace(/\r\n|\r/g, "\n").split("\n");
+ }
+ else
+ this.$split = function(text) {
+ return text.split(/\r\n|\r|\n/);
+ };
+ this.$detectNewLine = function(text) {
+ var match = text.match(/^.*?(\r\n|\r|\n)/m);
+ if (match) {
+ this.$autoNewLine = match[1];
+ } else {
+ this.$autoNewLine = "\n";
+ }
+ };
+ this.getNewLineCharacter = function() {
+ switch (this.$newLineMode) {
+ case "windows":
+ return "\r\n";
+
+ case "unix":
+ return "\n";
+
+ case "auto":
+ return this.$autoNewLine;
+ }
+ };
+
+ this.$autoNewLine = "\n";
+ this.$newLineMode = "auto";
+ this.setNewLineMode = function(newLineMode) {
+ if (this.$newLineMode === newLineMode)
+ return;
+
+ this.$newLineMode = newLineMode;
+ };
+ this.getNewLineMode = function() {
+ return this.$newLineMode;
+ };
+ this.isNewLine = function(text) {
+ return (text == "\r\n" || text == "\r" || text == "\n");
+ };
+ this.getLine = function(row) {
+ return this.$lines[row] || "";
+ };
+ this.getLines = function(firstRow, lastRow) {
+ return this.$lines.slice(firstRow, lastRow + 1);
+ };
+ this.getAllLines = function() {
+ return this.getLines(0, this.getLength());
+ };
+ this.getLength = function() {
+ return this.$lines.length;
+ };
+ this.getTextRange = function(range) {
+ if (range.start.row == range.end.row) {
+ return this.$lines[range.start.row].substring(range.start.column,
+ range.end.column);
+ }
+ else {
+ var lines = this.getLines(range.start.row+1, range.end.row-1);
+ lines.unshift((this.$lines[range.start.row] || "").substring(range.start.column));
+ lines.push((this.$lines[range.end.row] || "").substring(0, range.end.column));
+ return lines.join(this.getNewLineCharacter());
+ }
+ };
+ this.$clipPosition = function(position) {
+ var length = this.getLength();
+ if (position.row >= length) {
+ position.row = Math.max(0, length - 1);
+ position.column = this.getLine(length-1).length;
+ }
+ return position;
+ };
+ this.insert = function(position, text) {
+ if (!text || text.length === 0)
+ return position;
+
+ position = this.$clipPosition(position);
+
+ // only detect new lines if the document has no line break yet
+ if (this.getLength() <= 1)
+ this.$detectNewLine(text);
+
+ var lines = this.$split(text);
+ var firstLine = lines.splice(0, 1)[0];
+ var lastLine = lines.length == 0 ? null : lines.splice(lines.length - 1, 1)[0];
+
+ position = this.insertInLine(position, firstLine);
+ if (lastLine !== null) {
+ position = this.insertNewLine(position); // terminate first line
+ position = this.insertLines(position.row, lines);
+ position = this.insertInLine(position, lastLine || "");
+ }
+ return position;
+ };
+ /**
+ * Document@change(e)
+ * - e (Object): Contains at least one property called `"action"`. `"action"` indicates the action that triggered the change. Each action also has a set of additional properties.
+ *
+ * Fires whenever the document changes.
+ *
+ * Several methods trigger different `"change"` events. Below is a list of each action type, followed by each property that's also available:
+ *
+ * * `"insertLines"` (emitted by [[Document.insertLines]])
+ * * `range`: the [[Range]] of the change within the document
+ * * `lines`: the lines in the document that are changing
+ * * `"insertText"` (emitted by [[Document.insertNewLine]])
+ * * `range`: the [[Range]] of the change within the document
+ * * `text`: the text that's being added
+ * * `"removeLines"` (emitted by [[Document.insertLines]])
+ * * `range`: the [[Range]] of the change within the document
+ * * `lines`: the lines in the document that were removed
+ * * `nl`: the new line character (as defined by [[Document.getNewLineCharacter]])
+ * * `"removeText"` (emitted by [[Document.removeInLine]] and [[Document.removeNewLine]])
+ * * `range`: the [[Range]] of the change within the document
+ * * `text`: the text that's being removed
+ *
+ **/
+ this.insertLines = function(row, lines) {
+ if (lines.length == 0)
+ return {row: row, column: 0};
+
+ // apply doesn't work for big arrays (smallest threshold is on safari 0xFFFF)
+ // to circumvent that we have to break huge inserts into smaller chunks here
+ if (lines.length > 0xFFFF) {
+ var end = this.insertLines(row, lines.slice(0xFFFF));
+ lines = lines.slice(0, 0xFFFF);
+ }
+
+ var args = [row, 0];
+ args.push.apply(args, lines);
+ this.$lines.splice.apply(this.$lines, args);
+
+ var range = new Range(row, 0, row + lines.length, 0);
+ var delta = {
+ action: "insertLines",
+ range: range,
+ lines: lines
+ };
+ this._emit("change", { data: delta });
+ return end || range.end;
+ };
+ this.insertNewLine = function(position) {
+ position = this.$clipPosition(position);
+ var line = this.$lines[position.row] || "";
+
+ this.$lines[position.row] = line.substring(0, position.column);
+ this.$lines.splice(position.row + 1, 0, line.substring(position.column, line.length));
+
+ var end = {
+ row : position.row + 1,
+ column : 0
+ };
+
+ var delta = {
+ action: "insertText",
+ range: Range.fromPoints(position, end),
+ text: this.getNewLineCharacter()
+ };
+ this._emit("change", { data: delta });
+
+ return end;
+ };
+ this.insertInLine = function(position, text) {
+ if (text.length == 0)
+ return position;
+
+ var line = this.$lines[position.row] || "";
+
+ this.$lines[position.row] = line.substring(0, position.column) + text
+ + line.substring(position.column);
+
+ var end = {
+ row : position.row,
+ column : position.column + text.length
+ };
+
+ var delta = {
+ action: "insertText",
+ range: Range.fromPoints(position, end),
+ text: text
+ };
+ this._emit("change", { data: delta });
+
+ return end;
+ };
+ this.remove = function(range) {
+ // clip to document
+ range.start = this.$clipPosition(range.start);
+ range.end = this.$clipPosition(range.end);
+
+ if (range.isEmpty())
+ return range.start;
+
+ var firstRow = range.start.row;
+ var lastRow = range.end.row;
+
+ if (range.isMultiLine()) {
+ var firstFullRow = range.start.column == 0 ? firstRow : firstRow + 1;
+ var lastFullRow = lastRow - 1;
+
+ if (range.end.column > 0)
+ this.removeInLine(lastRow, 0, range.end.column);
+
+ if (lastFullRow >= firstFullRow)
+ this.removeLines(firstFullRow, lastFullRow);
+
+ if (firstFullRow != firstRow) {
+ this.removeInLine(firstRow, range.start.column, this.getLine(firstRow).length);
+ this.removeNewLine(range.start.row);
+ }
+ }
+ else {
+ this.removeInLine(firstRow, range.start.column, range.end.column);
+ }
+ return range.start;
+ };
+ this.removeInLine = function(row, startColumn, endColumn) {
+ if (startColumn == endColumn)
+ return;
+
+ var range = new Range(row, startColumn, row, endColumn);
+ var line = this.getLine(row);
+ var removed = line.substring(startColumn, endColumn);
+ var newLine = line.substring(0, startColumn) + line.substring(endColumn, line.length);
+ this.$lines.splice(row, 1, newLine);
+
+ var delta = {
+ action: "removeText",
+ range: range,
+ text: removed
+ };
+ this._emit("change", { data: delta });
+ return range.start;
+ };
+ this.removeLines = function(firstRow, lastRow) {
+ var range = new Range(firstRow, 0, lastRow + 1, 0);
+ var removed = this.$lines.splice(firstRow, lastRow - firstRow + 1);
+
+ var delta = {
+ action: "removeLines",
+ range: range,
+ nl: this.getNewLineCharacter(),
+ lines: removed
+ };
+ this._emit("change", { data: delta });
+ return removed;
+ };
+ this.removeNewLine = function(row) {
+ var firstLine = this.getLine(row);
+ var secondLine = this.getLine(row+1);
+
+ var range = new Range(row, firstLine.length, row+1, 0);
+ var line = firstLine + secondLine;
+
+ this.$lines.splice(row, 2, line);
+
+ var delta = {
+ action: "removeText",
+ range: range,
+ text: this.getNewLineCharacter()
+ };
+ this._emit("change", { data: delta });
+ };
+ this.replace = function(range, text) {
+ if (text.length == 0 && range.isEmpty())
+ return range.start;
+
+ // Shortcut: If the text we want to insert is the same as it is already
+ // in the document, we don't have to replace anything.
+ if (text == this.getTextRange(range))
+ return range.end;
+
+ this.remove(range);
+ if (text) {
+ var end = this.insert(range.start, text);
+ }
+ else {
+ end = range.start;
+ }
+
+ return end;
+ };
+ this.applyDeltas = function(deltas) {
+ for (var i=0; i=0; i--) {
+ var delta = deltas[i];
+
+ var range = Range.fromPoints(delta.range.start, delta.range.end);
+
+ if (delta.action == "insertLines")
+ this.removeLines(range.start.row, range.end.row - 1);
+ else if (delta.action == "insertText")
+ this.remove(range);
+ else if (delta.action == "removeLines")
+ this.insertLines(range.start.row, delta.lines);
+ else if (delta.action == "removeText")
+ this.insert(range.start, delta.text);
+ }
+ };
+
+}).call(Document.prototype);
+
+exports.Document = Document;
+});
+
+ace.define('ace/anchor', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/event_emitter'], function(require, exports, module) {
+
+
+var oop = require("./lib/oop");
+var EventEmitter = require("./lib/event_emitter").EventEmitter;
+
+/**
+ * new Anchor(doc, row, column)
+ * - doc (Document): The document to associate with the anchor
+ * - row (Number): The starting row position
+ * - column (Number): The starting column position
+ *
+ * Creates a new `Anchor` and associates it with a document.
+ *
+ **/
+
+var Anchor = exports.Anchor = function(doc, row, column) {
+ this.document = doc;
+
+ if (typeof column == "undefined")
+ this.setPosition(row.row, row.column);
+ else
+ this.setPosition(row, column);
+
+ this.$onChange = this.onChange.bind(this);
+ doc.on("change", this.$onChange);
+};
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.getPosition = function() {
+ return this.$clipPositionToDocument(this.row, this.column);
+ };
+
+ this.getDocument = function() {
+ return this.document;
+ };
+
+ this.onChange = function(e) {
+ var delta = e.data;
+ var range = delta.range;
+
+ if (range.start.row == range.end.row && range.start.row != this.row)
+ return;
+
+ if (range.start.row > this.row)
+ return;
+
+ if (range.start.row == this.row && range.start.column > this.column)
+ return;
+
+ var row = this.row;
+ var column = this.column;
+
+ if (delta.action === "insertText") {
+ if (range.start.row === row && range.start.column <= column) {
+ if (range.start.row === range.end.row) {
+ column += range.end.column - range.start.column;
+ }
+ else {
+ column -= range.start.column;
+ row += range.end.row - range.start.row;
+ }
+ }
+ else if (range.start.row !== range.end.row && range.start.row < row) {
+ row += range.end.row - range.start.row;
+ }
+ } else if (delta.action === "insertLines") {
+ if (range.start.row <= row) {
+ row += range.end.row - range.start.row;
+ }
+ }
+ else if (delta.action == "removeText") {
+ if (range.start.row == row && range.start.column < column) {
+ if (range.end.column >= column)
+ column = range.start.column;
+ else
+ column = Math.max(0, column - (range.end.column - range.start.column));
+
+ } else if (range.start.row !== range.end.row && range.start.row < row) {
+ if (range.end.row == row) {
+ column = Math.max(0, column - range.end.column) + range.start.column;
+ }
+ row -= (range.end.row - range.start.row);
+ }
+ else if (range.end.row == row) {
+ row -= range.end.row - range.start.row;
+ column = Math.max(0, column - range.end.column) + range.start.column;
+ }
+ } else if (delta.action == "removeLines") {
+ if (range.start.row <= row) {
+ if (range.end.row <= row)
+ row -= range.end.row - range.start.row;
+ else {
+ row = range.start.row;
+ column = 0;
+ }
+ }
+ }
+
+ this.setPosition(row, column, true);
+ };
+
+ this.setPosition = function(row, column, noClip) {
+ var pos;
+ if (noClip) {
+ pos = {
+ row: row,
+ column: column
+ };
+ }
+ else {
+ pos = this.$clipPositionToDocument(row, column);
+ }
+
+ if (this.row == pos.row && this.column == pos.column)
+ return;
+
+ var old = {
+ row: this.row,
+ column: this.column
+ };
+
+ this.row = pos.row;
+ this.column = pos.column;
+ this._emit("change", {
+ old: old,
+ value: pos
+ });
+ };
+
+ this.detach = function() {
+ this.document.removeEventListener("change", this.$onChange);
+ };
+
+ this.$clipPositionToDocument = function(row, column) {
+ var pos = {};
+
+ if (row >= this.document.getLength()) {
+ pos.row = Math.max(0, this.document.getLength() - 1);
+ pos.column = this.document.getLine(pos.row).length;
+ }
+ else if (row < 0) {
+ pos.row = 0;
+ pos.column = 0;
+ }
+ else {
+ pos.row = row;
+ pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column));
+ }
+
+ if (column < 0)
+ pos.column = 0;
+
+ return pos;
+ };
+
+}).call(Anchor.prototype);
+
+});
+
+ace.define('ace/background_tokenizer', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/event_emitter'], function(require, exports, module) {
+
+
+var oop = require("./lib/oop");
+var EventEmitter = require("./lib/event_emitter").EventEmitter;
+
+// tokenizing lines longer than this makes editor very slow
+var MAX_LINE_LENGTH = 5000;
+
+/**
+ * new BackgroundTokenizer(tokenizer, editor)
+ * - tokenizer (Tokenizer): The tokenizer to use
+ * - editor (Editor): The editor to associate with
+ *
+ * Creates a new `BackgroundTokenizer` object.
+ *
+ *
+ **/
+
+var BackgroundTokenizer = function(tokenizer, editor) {
+ this.running = false;
+ this.lines = [];
+ this.states = [];
+ this.currentLine = 0;
+ this.tokenizer = tokenizer;
+
+ var self = this;
+
+ this.$worker = function() {
+ if (!self.running) { return; }
+
+ var workerStart = new Date();
+ var startLine = self.currentLine;
+ var doc = self.doc;
+
+ var processedLines = 0;
+
+ var len = doc.getLength();
+ while (self.currentLine < len) {
+ self.$tokenizeRow(self.currentLine);
+ while (self.lines[self.currentLine])
+ self.currentLine++;
+
+ // only check every 5 lines
+ processedLines ++;
+ if ((processedLines % 5 == 0) && (new Date() - workerStart) > 20) {
+ self.fireUpdateEvent(startLine, self.currentLine-1);
+ self.running = setTimeout(self.$worker, 20);
+ return;
+ }
+ }
+
+ self.running = false;
+
+ self.fireUpdateEvent(startLine, len - 1);
+ };
+};
+
+(function(){
+
+ oop.implement(this, EventEmitter);
+ this.setTokenizer = function(tokenizer) {
+ this.tokenizer = tokenizer;
+ this.lines = [];
+ this.states = [];
+
+ this.start(0);
+ };
+ this.setDocument = function(doc) {
+ this.doc = doc;
+ this.lines = [];
+ this.states = [];
+
+ this.stop();
+ };
+ /**
+ * BackgroundTokenizer@update(e)
+ * - e (Object): An object containing two properties, `first` and `last`, which indicate the rows of the region being updated.
+ *
+ * Fires whenever the background tokeniziers between a range of rows are going to be updated.
+ *
+ **/
+ this.fireUpdateEvent = function(firstRow, lastRow) {
+ var data = {
+ first: firstRow,
+ last: lastRow
+ };
+ this._emit("update", {data: data});
+ };
+ this.start = function(startRow) {
+ this.currentLine = Math.min(startRow || 0, this.currentLine, this.doc.getLength());
+
+ // remove all cached items below this line
+ this.lines.splice(this.currentLine, this.lines.length);
+ this.states.splice(this.currentLine, this.states.length);
+
+ this.stop();
+ // pretty long delay to prevent the tokenizer from interfering with the user
+ this.running = setTimeout(this.$worker, 700);
+ };
+
+ this.$updateOnChange = function(delta) {
+ var range = delta.range;
+ var startRow = range.start.row;
+ var len = range.end.row - startRow;
+
+ if (len === 0) {
+ this.lines[startRow] = null;
+ } else if (delta.action == "removeText" || delta.action == "removeLines") {
+ this.lines.splice(startRow, len + 1, null);
+ this.states.splice(startRow, len + 1, null);
+ } else {
+ var args = Array(len + 1);
+ args.unshift(startRow, 1);
+ this.lines.splice.apply(this.lines, args);
+ this.states.splice.apply(this.states, args);
+ }
+
+ this.currentLine = Math.min(startRow, this.currentLine, this.doc.getLength());
+
+ this.stop();
+ // pretty long delay to prevent the tokenizer from interfering with the user
+ this.running = setTimeout(this.$worker, 700);
+ };
+ this.stop = function() {
+ if (this.running)
+ clearTimeout(this.running);
+ this.running = false;
+ };
+ this.getTokens = function(row) {
+ return this.lines[row] || this.$tokenizeRow(row);
+ };
+ this.getState = function(row) {
+ if (this.currentLine == row)
+ this.$tokenizeRow(row);
+ return this.states[row] || "start";
+ };
+
+ this.$tokenizeRow = function(row) {
+ var line = this.doc.getLine(row);
+ var state = this.states[row - 1];
+
+ if (line.length > MAX_LINE_LENGTH) {
+ var overflow = {value: line.substr(MAX_LINE_LENGTH), type: "text"};
+ line = line.slice(0, MAX_LINE_LENGTH);
+ }
+ var data = this.tokenizer.getLineTokens(line, state);
+ if (overflow) {
+ data.tokens.push(overflow);
+ data.state = "start";
+ }
+
+ if (this.states[row] !== data.state) {
+ this.states[row] = data.state;
+ this.lines[row + 1] = null;
+ if (this.currentLine > row + 1)
+ this.currentLine = row + 1;
+ } else if (this.currentLine == row) {
+ this.currentLine = row + 1;
+ }
+
+ return this.lines[row] = data.tokens;
+ };
+
+}).call(BackgroundTokenizer.prototype);
+
+exports.BackgroundTokenizer = BackgroundTokenizer;
+});
+
+ace.define('ace/search_highlight', ['require', 'exports', 'module' , 'ace/lib/lang', 'ace/lib/oop', 'ace/range'], function(require, exports, module) {
+
+
+var lang = require("./lib/lang");
+var oop = require("./lib/oop");
+var Range = require("./range").Range;
+
+var SearchHighlight = function(regExp, clazz, type) {
+ this.setRegexp(regExp);
+ this.clazz = clazz;
+ this.type = type || "text";
+};
+
+(function() {
+ this.setRegexp = function(regExp) {
+ if (this.regExp+"" == regExp+"")
+ return;
+ this.regExp = regExp;
+ this.cache = [];
+ };
+
+ this.update = function(html, markerLayer, session, config) {
+ if (!this.regExp)
+ return;
+ var start = config.firstRow, end = config.lastRow;
+
+ for (var i = start; i <= end; i++) {
+ var ranges = this.cache[i];
+ if (ranges == null) {
+ ranges = lang.getMatchOffsets(session.getLine(i), this.regExp);
+ ranges = ranges.map(function(match) {
+ return new Range(i, match.offset, i, match.offset + match.length);
+ });
+ this.cache[i] = ranges.length ? ranges : "";
+ }
+
+ for (var j = ranges.length; j --; ) {
+ markerLayer.drawSingleLineMarker(
+ html, ranges[j].toScreenRange(session), this.clazz, config,
+ null, this.type
+ );
+ }
+ }
+ };
+
+}).call(SearchHighlight.prototype);
+
+exports.SearchHighlight = SearchHighlight;
+});
+
+ace.define('ace/edit_session/folding', ['require', 'exports', 'module' , 'ace/range', 'ace/edit_session/fold_line', 'ace/edit_session/fold', 'ace/token_iterator'], function(require, exports, module) {
+
+
+var Range = require("../range").Range;
+var FoldLine = require("./fold_line").FoldLine;
+var Fold = require("./fold").Fold;
+var TokenIterator = require("../token_iterator").TokenIterator;
+
+function Folding() {
+ /*
+ * Looks up a fold at a given row/column. Possible values for side:
+ * -1: ignore a fold if fold.start = row/column
+ * +1: ignore a fold if fold.end = row/column
+ */
+ this.getFoldAt = function(row, column, side) {
+ var foldLine = this.getFoldLine(row);
+ if (!foldLine)
+ return null;
+
+ var folds = foldLine.folds;
+ for (var i = 0; i < folds.length; i++) {
+ var fold = folds[i];
+ if (fold.range.contains(row, column)) {
+ if (side == 1 && fold.range.isEnd(row, column)) {
+ continue;
+ } else if (side == -1 && fold.range.isStart(row, column)) {
+ continue;
+ }
+ return fold;
+ }
+ }
+ };
+ this.getFoldsInRange = function(range) {
+ range = range.clone();
+ var start = range.start;
+ var end = range.end;
+ var foldLines = this.$foldData;
+ var foundFolds = [];
+
+ start.column += 1;
+ end.column -= 1;
+
+ for (var i = 0; i < foldLines.length; i++) {
+ var cmp = foldLines[i].range.compareRange(range);
+ if (cmp == 2) {
+ // Range is before foldLine. No intersection. This means,
+ // there might be other foldLines that intersect.
+ continue;
+ }
+ else if (cmp == -2) {
+ // Range is after foldLine. There can't be any other foldLines then,
+ // so let's give up.
+ break;
+ }
+
+ var folds = foldLines[i].folds;
+ for (var j = 0; j < folds.length; j++) {
+ var fold = folds[j];
+ cmp = fold.range.compareRange(range);
+ if (cmp == -2) {
+ break;
+ } else if (cmp == 2) {
+ continue;
+ } else
+ // WTF-state: Can happen due to -1/+1 to start/end column.
+ if (cmp == 42) {
+ break;
+ }
+ foundFolds.push(fold);
+ }
+ }
+ return foundFolds;
+ };
+ this.getAllFolds = function() {
+ var folds = [];
+ var foldLines = this.$foldData;
+
+ function addFold(fold) {
+ folds.push(fold);
+ if (!fold.subFolds)
+ return;
+
+ for (var i = 0; i < fold.subFolds.length; i++)
+ addFold(fold.subFolds[i]);
+ }
+
+ for (var i = 0; i < foldLines.length; i++)
+ for (var j = 0; j < foldLines[i].folds.length; j++)
+ addFold(foldLines[i].folds[j]);
+
+ return folds;
+ };
+ this.getFoldStringAt = function(row, column, trim, foldLine) {
+ foldLine = foldLine || this.getFoldLine(row);
+ if (!foldLine)
+ return null;
+
+ var lastFold = {
+ end: { column: 0 }
+ };
+ // TODO: Refactor to use getNextFoldTo function.
+ var str, fold;
+ for (var i = 0; i < foldLine.folds.length; i++) {
+ fold = foldLine.folds[i];
+ var cmp = fold.range.compareEnd(row, column);
+ if (cmp == -1) {
+ str = this
+ .getLine(fold.start.row)
+ .substring(lastFold.end.column, fold.start.column);
+ break;
+ }
+ else if (cmp === 0) {
+ return null;
+ }
+ lastFold = fold;
+ }
+ if (!str)
+ str = this.getLine(fold.start.row).substring(lastFold.end.column);
+
+ if (trim == -1)
+ return str.substring(0, column - lastFold.end.column);
+ else if (trim == 1)
+ return str.substring(column - lastFold.end.column);
+ else
+ return str;
+ };
+
+ this.getFoldLine = function(docRow, startFoldLine) {
+ var foldData = this.$foldData;
+ var i = 0;
+ if (startFoldLine)
+ i = foldData.indexOf(startFoldLine);
+ if (i == -1)
+ i = 0;
+ for (i; i < foldData.length; i++) {
+ var foldLine = foldData[i];
+ if (foldLine.start.row <= docRow && foldLine.end.row >= docRow) {
+ return foldLine;
+ } else if (foldLine.end.row > docRow) {
+ return null;
+ }
+ }
+ return null;
+ };
+
+ // returns the fold which starts after or contains docRow
+ this.getNextFoldLine = function(docRow, startFoldLine) {
+ var foldData = this.$foldData;
+ var i = 0;
+ if (startFoldLine)
+ i = foldData.indexOf(startFoldLine);
+ if (i == -1)
+ i = 0;
+ for (i; i < foldData.length; i++) {
+ var foldLine = foldData[i];
+ if (foldLine.end.row >= docRow) {
+ return foldLine;
+ }
+ }
+ return null;
+ };
+
+ this.getFoldedRowCount = function(first, last) {
+ var foldData = this.$foldData, rowCount = last-first+1;
+ for (var i = 0; i < foldData.length; i++) {
+ var foldLine = foldData[i],
+ end = foldLine.end.row,
+ start = foldLine.start.row;
+ if (end >= last) {
+ if(start < last) {
+ if(start >= first)
+ rowCount -= last-start;
+ else
+ rowCount = 0;//in one fold
+ }
+ break;
+ } else if(end >= first){
+ if (start >= first) //fold inside range
+ rowCount -= end-start;
+ else
+ rowCount -= end-first+1;
+ }
+ }
+ return rowCount;
+ };
+
+ this.$addFoldLine = function(foldLine) {
+ this.$foldData.push(foldLine);
+ this.$foldData.sort(function(a, b) {
+ return a.start.row - b.start.row;
+ });
+ return foldLine;
+ };
+ this.addFold = function(placeholder, range) {
+ var foldData = this.$foldData;
+ var added = false;
+ var fold;
+
+ if (placeholder instanceof Fold)
+ fold = placeholder;
+ else
+ fold = new Fold(range, placeholder);
+
+ this.$clipRangeToDocument(fold.range);
+
+ var startRow = fold.start.row;
+ var startColumn = fold.start.column;
+ var endRow = fold.end.row;
+ var endColumn = fold.end.column;
+
+ // --- Some checking ---
+ if (fold.placeholder.length < 2)
+ throw "Placeholder has to be at least 2 characters";
+
+ if (startRow == endRow && endColumn - startColumn < 2)
+ throw "The range has to be at least 2 characters width";
+
+ var startFold = this.getFoldAt(startRow, startColumn, 1);
+ var endFold = this.getFoldAt(endRow, endColumn, -1);
+ if (startFold && endFold == startFold)
+ return startFold.addSubFold(fold);
+
+ if (
+ (startFold && !startFold.range.isStart(startRow, startColumn))
+ || (endFold && !endFold.range.isEnd(endRow, endColumn))
+ ) {
+ throw "A fold can't intersect already existing fold" + fold.range + startFold.range;
+ }
+
+ // Check if there are folds in the range we create the new fold for.
+ var folds = this.getFoldsInRange(fold.range);
+ if (folds.length > 0) {
+ // Remove the folds from fold data.
+ this.removeFolds(folds);
+ // Add the removed folds as subfolds on the new fold.
+ fold.subFolds = folds;
+ }
+
+ for (var i = 0; i < foldData.length; i++) {
+ var foldLine = foldData[i];
+ if (endRow == foldLine.start.row) {
+ foldLine.addFold(fold);
+ added = true;
+ break;
+ }
+ else if (startRow == foldLine.end.row) {
+ foldLine.addFold(fold);
+ added = true;
+ if (!fold.sameRow) {
+ // Check if we might have to merge two FoldLines.
+ var foldLineNext = foldData[i + 1];
+ if (foldLineNext && foldLineNext.start.row == endRow) {
+ // We need to merge!
+ foldLine.merge(foldLineNext);
+ break;
+ }
+ }
+ break;
+ }
+ else if (endRow <= foldLine.start.row) {
+ break;
+ }
+ }
+
+ if (!added)
+ foldLine = this.$addFoldLine(new FoldLine(this.$foldData, fold));
+
+ if (this.$useWrapMode)
+ this.$updateWrapData(foldLine.start.row, foldLine.start.row);
+ else
+ this.$updateRowLengthCache(foldLine.start.row, foldLine.start.row);
+
+ // Notify that fold data has changed.
+ this.$modified = true;
+ this._emit("changeFold", { data: fold });
+
+ return fold;
+ };
+
+ this.addFolds = function(folds) {
+ folds.forEach(function(fold) {
+ this.addFold(fold);
+ }, this);
+ };
+
+ this.removeFold = function(fold) {
+ var foldLine = fold.foldLine;
+ var startRow = foldLine.start.row;
+ var endRow = foldLine.end.row;
+
+ var foldLines = this.$foldData;
+ var folds = foldLine.folds;
+ // Simple case where there is only one fold in the FoldLine such that
+ // the entire fold line can get removed directly.
+ if (folds.length == 1) {
+ foldLines.splice(foldLines.indexOf(foldLine), 1);
+ } else
+ // If the fold is the last fold of the foldLine, just remove it.
+ if (foldLine.range.isEnd(fold.end.row, fold.end.column)) {
+ folds.pop();
+ foldLine.end.row = folds[folds.length - 1].end.row;
+ foldLine.end.column = folds[folds.length - 1].end.column;
+ } else
+ // If the fold is the first fold of the foldLine, just remove it.
+ if (foldLine.range.isStart(fold.start.row, fold.start.column)) {
+ folds.shift();
+ foldLine.start.row = folds[0].start.row;
+ foldLine.start.column = folds[0].start.column;
+ } else
+ // We know there are more then 2 folds and the fold is not at the edge.
+ // This means, the fold is somewhere in between.
+ //
+ // If the fold is in one row, we just can remove it.
+ if (fold.sameRow) {
+ folds.splice(folds.indexOf(fold), 1);
+ } else
+ // The fold goes over more then one row. This means remvoing this fold
+ // will cause the fold line to get splitted up. newFoldLine is the second part
+ {
+ var newFoldLine = foldLine.split(fold.start.row, fold.start.column);
+ folds = newFoldLine.folds;
+ folds.shift();
+ newFoldLine.start.row = folds[0].start.row;
+ newFoldLine.start.column = folds[0].start.column;
+ }
+
+ if (this.$useWrapMode)
+ this.$updateWrapData(startRow, endRow);
+ else
+ this.$updateRowLengthCache(startRow, endRow);
+
+ // Notify that fold data has changed.
+ this.$modified = true;
+ this._emit("changeFold", { data: fold });
+ };
+
+ this.removeFolds = function(folds) {
+ // We need to clone the folds array passed in as it might be the folds
+ // array of a fold line and as we call this.removeFold(fold), folds
+ // are removed from folds and changes the current index.
+ var cloneFolds = [];
+ for (var i = 0; i < folds.length; i++) {
+ cloneFolds.push(folds[i]);
+ }
+
+ cloneFolds.forEach(function(fold) {
+ this.removeFold(fold);
+ }, this);
+ this.$modified = true;
+ };
+
+ this.expandFold = function(fold) {
+ this.removeFold(fold);
+ fold.subFolds.forEach(function(fold) {
+ this.addFold(fold);
+ }, this);
+ fold.subFolds = [];
+ };
+
+ this.expandFolds = function(folds) {
+ folds.forEach(function(fold) {
+ this.expandFold(fold);
+ }, this);
+ };
+
+ this.unfold = function(location, expandInner) {
+ var range, folds;
+ if (location == null)
+ range = new Range(0, 0, this.getLength(), 0);
+ else if (typeof location == "number")
+ range = new Range(location, 0, location, this.getLine(location).length);
+ else if ("row" in location)
+ range = Range.fromPoints(location, location);
+ else
+ range = location;
+
+ folds = this.getFoldsInRange(range);
+ if (expandInner) {
+ this.removeFolds(folds);
+ } else {
+ // TODO: might need to remove and add folds in one go instead of using
+ // expandFolds several times.
+ while (folds.length) {
+ this.expandFolds(folds);
+ folds = this.getFoldsInRange(range);
+ }
+ }
+ };
+ this.isRowFolded = function(docRow, startFoldRow) {
+ return !!this.getFoldLine(docRow, startFoldRow);
+ };
+
+ this.getRowFoldEnd = function(docRow, startFoldRow) {
+ var foldLine = this.getFoldLine(docRow, startFoldRow);
+ return foldLine ? foldLine.end.row : docRow;
+ };
+
+ this.getFoldDisplayLine = function(foldLine, endRow, endColumn, startRow, startColumn) {
+ if (startRow == null) {
+ startRow = foldLine.start.row;
+ startColumn = 0;
+ }
+
+ if (endRow == null) {
+ endRow = foldLine.end.row;
+ endColumn = this.getLine(endRow).length;
+ }
+
+ // Build the textline using the FoldLine walker.
+ var doc = this.doc;
+ var textLine = "";
+
+ foldLine.walk(function(placeholder, row, column, lastColumn) {
+ if (row < startRow) {
+ return;
+ } else if (row == startRow) {
+ if (column < startColumn) {
+ return;
+ }
+ lastColumn = Math.max(startColumn, lastColumn);
+ }
+ if (placeholder) {
+ textLine += placeholder;
+ } else {
+ textLine += doc.getLine(row).substring(lastColumn, column);
+ }
+ }.bind(this), endRow, endColumn);
+ return textLine;
+ };
+
+ this.getDisplayLine = function(row, endColumn, startRow, startColumn) {
+ var foldLine = this.getFoldLine(row);
+
+ if (!foldLine) {
+ var line;
+ line = this.doc.getLine(row);
+ return line.substring(startColumn || 0, endColumn || line.length);
+ } else {
+ return this.getFoldDisplayLine(
+ foldLine, row, endColumn, startRow, startColumn);
+ }
+ };
+
+ this.$cloneFoldData = function() {
+ var fd = [];
+ fd = this.$foldData.map(function(foldLine) {
+ var folds = foldLine.folds.map(function(fold) {
+ return fold.clone();
+ });
+ return new FoldLine(fd, folds);
+ });
+
+ return fd;
+ };
+
+ this.toggleFold = function(tryToUnfold) {
+ var selection = this.selection;
+ var range = selection.getRange();
+ var fold;
+ var bracketPos;
+
+ if (range.isEmpty()) {
+ var cursor = range.start;
+ fold = this.getFoldAt(cursor.row, cursor.column);
+
+ if (fold) {
+ this.expandFold(fold);
+ return;
+ }
+ else if (bracketPos = this.findMatchingBracket(cursor)) {
+ if (range.comparePoint(bracketPos) == 1) {
+ range.end = bracketPos;
+ }
+ else {
+ range.start = bracketPos;
+ range.start.column++;
+ range.end.column--;
+ }
+ }
+ else if (bracketPos = this.findMatchingBracket({row: cursor.row, column: cursor.column + 1})) {
+ if (range.comparePoint(bracketPos) == 1)
+ range.end = bracketPos;
+ else
+ range.start = bracketPos;
+
+ range.start.column++;
+ }
+ else {
+ range = this.getCommentFoldRange(cursor.row, cursor.column) || range;
+ }
+ } else {
+ var folds = this.getFoldsInRange(range);
+ if (tryToUnfold && folds.length) {
+ this.expandFolds(folds);
+ return;
+ }
+ else if (folds.length == 1 ) {
+ fold = folds[0];
+ }
+ }
+
+ if (!fold)
+ fold = this.getFoldAt(range.start.row, range.start.column);
+
+ if (fold && fold.range.toString() == range.toString()) {
+ this.expandFold(fold);
+ return;
+ }
+
+ var placeholder = "...";
+ if (!range.isMultiLine()) {
+ placeholder = this.getTextRange(range);
+ if(placeholder.length < 4)
+ return;
+ placeholder = placeholder.trim().substring(0, 2) + "..";
+ }
+
+ this.addFold(placeholder, range);
+ };
+
+ this.getCommentFoldRange = function(row, column) {
+ var iterator = new TokenIterator(this, row, column);
+ var token = iterator.getCurrentToken();
+ if (token && /^comment|string/.test(token.type)) {
+ var range = new Range();
+ var re = new RegExp(token.type.replace(/\..*/, "\\."));
+ do {
+ token = iterator.stepBackward();
+ } while(token && re.test(token.type));
+
+ iterator.stepForward();
+ range.start.row = iterator.getCurrentTokenRow();
+ range.start.column = iterator.getCurrentTokenColumn() + 2;
+
+ iterator = new TokenIterator(this, row, column);
+
+ do {
+ token = iterator.stepForward();
+ } while(token && re.test(token.type));
+
+ token = iterator.stepBackward();
+
+ range.end.row = iterator.getCurrentTokenRow();
+ range.end.column = iterator.getCurrentTokenColumn() + token.value.length;
+ return range;
+ }
+ };
+
+ this.foldAll = function(startRow, endRow) {
+ var foldWidgets = this.foldWidgets;
+ endRow = endRow || this.getLength();
+ for (var row = startRow || 0; row < endRow; row++) {
+ if (foldWidgets[row] == null)
+ foldWidgets[row] = this.getFoldWidget(row);
+ if (foldWidgets[row] != "start")
+ continue;
+
+ var range = this.getFoldWidgetRange(row);
+ // sometimes range can be incompatible with existing fold
+ // wouldn't it be better for addFold to return null istead of throwing?
+ if (range && range.end.row < endRow) try {
+ this.addFold("...", range);
+ } catch(e) {}
+ }
+ };
+
+ this.$foldStyles = {
+ "manual": 1,
+ "markbegin": 1,
+ "markbeginend": 1
+ };
+ this.$foldStyle = "markbegin";
+ this.setFoldStyle = function(style) {
+ if (!this.$foldStyles[style])
+ throw new Error("invalid fold style: " + style + "[" + Object.keys(this.$foldStyles).join(", ") + "]");
+
+ if (this.$foldStyle == style)
+ return;
+
+ this.$foldStyle = style;
+
+ if (style == "manual")
+ this.unfold();
+
+ // reset folding
+ var mode = this.$foldMode;
+ this.$setFolding(null);
+ this.$setFolding(mode);
+ };
+
+ // structured folding
+ this.$setFolding = function(foldMode) {
+ if (this.$foldMode == foldMode)
+ return;
+
+ this.$foldMode = foldMode;
+
+ this.removeListener('change', this.$updateFoldWidgets);
+ this._emit("changeAnnotation");
+
+ if (!foldMode || this.$foldStyle == "manual") {
+ this.foldWidgets = null;
+ return;
+ }
+
+ this.foldWidgets = [];
+ this.getFoldWidget = foldMode.getFoldWidget.bind(foldMode, this, this.$foldStyle);
+ this.getFoldWidgetRange = foldMode.getFoldWidgetRange.bind(foldMode, this, this.$foldStyle);
+
+ this.$updateFoldWidgets = this.updateFoldWidgets.bind(this);
+ this.on('change', this.$updateFoldWidgets);
+
+ };
+
+ this.onFoldWidgetClick = function(row, e) {
+ var type = this.getFoldWidget(row);
+ var line = this.getLine(row);
+ var onlySubfolds = e.shiftKey;
+ var addSubfolds = onlySubfolds || e.ctrlKey || e.altKey || e.metaKey;
+ var fold;
+
+ if (type == "end")
+ fold = this.getFoldAt(row, 0, -1);
+ else
+ fold = this.getFoldAt(row, line.length, 1);
+
+ if (fold) {
+ if (addSubfolds)
+ this.removeFold(fold);
+ else
+ this.expandFold(fold);
+ return;
+ }
+
+ var range = this.getFoldWidgetRange(row);
+ if (range) {
+ // sometimes singleline folds can be missed by the code above
+ if (!range.isMultiLine()) {
+ fold = this.getFoldAt(range.start.row, range.start.column, 1);
+ if (fold && range.isEqual(fold.range)) {
+ this.removeFold(fold);
+ return;
+ }
+ }
+
+ if (!onlySubfolds)
+ this.addFold("...", range);
+
+ if (addSubfolds)
+ this.foldAll(range.start.row + 1, range.end.row);
+ } else {
+ if (addSubfolds)
+ this.foldAll(row + 1, this.getLength());
+ (e.target || e.srcElement).className += " invalid"
+ }
+ };
+
+ this.updateFoldWidgets = function(e) {
+ var delta = e.data;
+ var range = delta.range;
+ var firstRow = range.start.row;
+ var len = range.end.row - firstRow;
+
+ if (len === 0) {
+ this.foldWidgets[firstRow] = null;
+ } else if (delta.action == "removeText" || delta.action == "removeLines") {
+ this.foldWidgets.splice(firstRow, len + 1, null);
+ } else {
+ var args = Array(len + 1);
+ args.unshift(firstRow, 1);
+ this.foldWidgets.splice.apply(this.foldWidgets, args);
+ }
+ };
+
+}
+
+exports.Folding = Folding;
+
+});
+
+ace.define('ace/edit_session/fold_line', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) {
+
+
+var Range = require("../range").Range;
+function FoldLine(foldData, folds) {
+ this.foldData = foldData;
+ if (Array.isArray(folds)) {
+ this.folds = folds;
+ } else {
+ folds = this.folds = [ folds ];
+ }
+
+ var last = folds[folds.length - 1]
+ this.range = new Range(folds[0].start.row, folds[0].start.column,
+ last.end.row, last.end.column);
+ this.start = this.range.start;
+ this.end = this.range.end;
+
+ this.folds.forEach(function(fold) {
+ fold.setFoldLine(this);
+ }, this);
+}
+
+(function() {
+ /*
+ * Note: This doesn't update wrapData!
+ */
+ this.shiftRow = function(shift) {
+ this.start.row += shift;
+ this.end.row += shift;
+ this.folds.forEach(function(fold) {
+ fold.start.row += shift;
+ fold.end.row += shift;
+ });
+ }
+
+ this.addFold = function(fold) {
+ if (fold.sameRow) {
+ if (fold.start.row < this.startRow || fold.endRow > this.endRow) {
+ throw "Can't add a fold to this FoldLine as it has no connection";
+ }
+ this.folds.push(fold);
+ this.folds.sort(function(a, b) {
+ return -a.range.compareEnd(b.start.row, b.start.column);
+ });
+ if (this.range.compareEnd(fold.start.row, fold.start.column) > 0) {
+ this.end.row = fold.end.row;
+ this.end.column = fold.end.column;
+ } else if (this.range.compareStart(fold.end.row, fold.end.column) < 0) {
+ this.start.row = fold.start.row;
+ this.start.column = fold.start.column;
+ }
+ } else if (fold.start.row == this.end.row) {
+ this.folds.push(fold);
+ this.end.row = fold.end.row;
+ this.end.column = fold.end.column;
+ } else if (fold.end.row == this.start.row) {
+ this.folds.unshift(fold);
+ this.start.row = fold.start.row;
+ this.start.column = fold.start.column;
+ } else {
+ throw "Trying to add fold to FoldRow that doesn't have a matching row";
+ }
+ fold.foldLine = this;
+ }
+
+ this.containsRow = function(row) {
+ return row >= this.start.row && row <= this.end.row;
+ }
+
+ this.walk = function(callback, endRow, endColumn) {
+ var lastEnd = 0,
+ folds = this.folds,
+ fold,
+ comp, stop, isNewRow = true;
+
+ if (endRow == null) {
+ endRow = this.end.row;
+ endColumn = this.end.column;
+ }
+
+ for (var i = 0; i < folds.length; i++) {
+ fold = folds[i];
+
+ comp = fold.range.compareStart(endRow, endColumn);
+ // This fold is after the endRow/Column.
+ if (comp == -1) {
+ callback(null, endRow, endColumn, lastEnd, isNewRow);
+ return;
+ }
+
+ stop = callback(null, fold.start.row, fold.start.column, lastEnd, isNewRow);
+ stop = !stop && callback(fold.placeholder, fold.start.row, fold.start.column, lastEnd);
+
+ // If the user requested to stop the walk or endRow/endColumn is
+ // inside of this fold (comp == 0), then end here.
+ if (stop || comp == 0) {
+ return;
+ }
+
+ // Note the new lastEnd might not be on the same line. However,
+ // it's the callback's job to recognize this.
+ isNewRow = !fold.sameRow;
+ lastEnd = fold.end.column;
+ }
+ callback(null, endRow, endColumn, lastEnd, isNewRow);
+ }
+
+ this.getNextFoldTo = function(row, column) {
+ var fold, cmp;
+ for (var i = 0; i < this.folds.length; i++) {
+ fold = this.folds[i];
+ cmp = fold.range.compareEnd(row, column);
+ if (cmp == -1) {
+ return {
+ fold: fold,
+ kind: "after"
+ };
+ } else if (cmp == 0) {
+ return {
+ fold: fold,
+ kind: "inside"
+ }
+ }
+ }
+ return null;
+ }
+
+ this.addRemoveChars = function(row, column, len) {
+ var ret = this.getNextFoldTo(row, column),
+ fold, folds;
+ if (ret) {
+ fold = ret.fold;
+ if (ret.kind == "inside"
+ && fold.start.column != column
+ && fold.start.row != row)
+ {
+ //throwing here breaks whole editor
+ //TODO: properly handle this
+ window.console && window.console.log(row, column, fold);
+ } else if (fold.start.row == row) {
+ folds = this.folds;
+ var i = folds.indexOf(fold);
+ if (i == 0) {
+ this.start.column += len;
+ }
+ for (i; i < folds.length; i++) {
+ fold = folds[i];
+ fold.start.column += len;
+ if (!fold.sameRow) {
+ return;
+ }
+ fold.end.column += len;
+ }
+ this.end.column += len;
+ }
+ }
+ }
+
+ this.split = function(row, column) {
+ var fold = this.getNextFoldTo(row, column).fold,
+ folds = this.folds;
+ var foldData = this.foldData;
+
+ if (!fold) {
+ return null;
+ }
+ var i = folds.indexOf(fold);
+ var foldBefore = folds[i - 1];
+ this.end.row = foldBefore.end.row;
+ this.end.column = foldBefore.end.column;
+
+ // Remove the folds after row/column and create a new FoldLine
+ // containing these removed folds.
+ folds = folds.splice(i, folds.length - i);
+
+ var newFoldLine = new FoldLine(foldData, folds);
+ foldData.splice(foldData.indexOf(this) + 1, 0, newFoldLine);
+ return newFoldLine;
+ }
+
+ this.merge = function(foldLineNext) {
+ var folds = foldLineNext.folds;
+ for (var i = 0; i < folds.length; i++) {
+ this.addFold(folds[i]);
+ }
+ // Remove the foldLineNext - no longer needed, as
+ // it's merged now with foldLineNext.
+ var foldData = this.foldData;
+ foldData.splice(foldData.indexOf(foldLineNext), 1);
+ }
+
+ this.toString = function() {
+ var ret = [this.range.toString() + ": [" ];
+
+ this.folds.forEach(function(fold) {
+ ret.push(" " + fold.toString());
+ });
+ ret.push("]")
+ return ret.join("\n");
+ }
+
+ this.idxToPosition = function(idx) {
+ var lastFoldEndColumn = 0;
+ var fold;
+
+ for (var i = 0; i < this.folds.length; i++) {
+ var fold = this.folds[i];
+
+ idx -= fold.start.column - lastFoldEndColumn;
+ if (idx < 0) {
+ return {
+ row: fold.start.row,
+ column: fold.start.column + idx
+ };
+ }
+
+ idx -= fold.placeholder.length;
+ if (idx < 0) {
+ return fold.start;
+ }
+
+ lastFoldEndColumn = fold.end.column;
+ }
+
+ return {
+ row: this.end.row,
+ column: this.end.column + idx
+ };
+ }
+}).call(FoldLine.prototype);
+
+exports.FoldLine = FoldLine;
+});
+
+ace.define('ace/edit_session/fold', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+/*
+ * Simple fold-data struct.
+ **/
+var Fold = exports.Fold = function(range, placeholder) {
+ this.foldLine = null;
+ this.placeholder = placeholder;
+ this.range = range;
+ this.start = range.start;
+ this.end = range.end;
+
+ this.sameRow = range.start.row == range.end.row;
+ this.subFolds = [];
+};
+
+(function() {
+
+ this.toString = function() {
+ return '"' + this.placeholder + '" ' + this.range.toString();
+ };
+
+ this.setFoldLine = function(foldLine) {
+ this.foldLine = foldLine;
+ this.subFolds.forEach(function(fold) {
+ fold.setFoldLine(foldLine);
+ });
+ };
+
+ this.clone = function() {
+ var range = this.range.clone();
+ var fold = new Fold(range, this.placeholder);
+ this.subFolds.forEach(function(subFold) {
+ fold.subFolds.push(subFold.clone());
+ });
+ return fold;
+ };
+
+ this.addSubFold = function(fold) {
+ if (this.range.isEqual(fold))
+ return this;
+
+ if (!this.range.containsRange(fold))
+ throw "A fold can't intersect already existing fold" + fold.range + this.range;
+
+ var row = fold.range.start.row, column = fold.range.start.column;
+ for (var i = 0, cmp = -1; i < this.subFolds.length; i++) {
+ cmp = this.subFolds[i].range.compare(row, column);
+ if (cmp != 1)
+ break;
+ }
+ var afterStart = this.subFolds[i];
+
+ if (cmp == 0)
+ return afterStart.addSubFold(fold);
+
+ // cmp == -1
+ var row = fold.range.end.row, column = fold.range.end.column;
+ for (var j = i, cmp = -1; j < this.subFolds.length; j++) {
+ cmp = this.subFolds[j].range.compare(row, column);
+ if (cmp != 1)
+ break;
+ }
+ var afterEnd = this.subFolds[j];
+
+ if (cmp == 0)
+ throw "A fold can't intersect already existing fold" + fold.range + this.range;
+
+ var consumedFolds = this.subFolds.splice(i, j - i, fold);
+ fold.setFoldLine(this.foldLine);
+
+ return fold;
+ };
+
+}).call(Fold.prototype);
+
+});
+
+ace.define('ace/token_iterator', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+/**
+ * class TokenIterator
+ *
+ * This class provides an essay way to treat the document as a stream of tokens, and provides methods to iterate over these tokens.
+ *
+ **/
+
+/**
+ * new TokenIterator(session, initialRow, initialColumn)
+ * - session (EditSession): The session to associate with
+ * - initialRow (Number): The row to start the tokenizing at
+ * - initialColumn (Number): The column to start the tokenizing at
+ *
+ * Creates a new token iterator object. The inital token index is set to the provided row and column coordinates.
+ *
+ **/
+var TokenIterator = function(session, initialRow, initialColumn) {
+ this.$session = session;
+ this.$row = initialRow;
+ this.$rowTokens = session.getTokens(initialRow);
+
+ var token = session.getTokenAt(initialRow, initialColumn);
+ this.$tokenIndex = token ? token.index : -1;
+};
+
+(function() {
+
+ /**
+ * TokenIterator.stepBackward() -> [String]
+ * + (String): If the current point is not at the top of the file, this function returns `null`. Otherwise, it returns an array of the tokenized strings.
+ *
+ * Tokenizes all the items from the current point to the row prior in the document.
+ **/
+ this.stepBackward = function() {
+ this.$tokenIndex -= 1;
+
+ while (this.$tokenIndex < 0) {
+ this.$row -= 1;
+ if (this.$row < 0) {
+ this.$row = 0;
+ return null;
+ }
+
+ this.$rowTokens = this.$session.getTokens(this.$row);
+ this.$tokenIndex = this.$rowTokens.length - 1;
+ }
+
+ return this.$rowTokens[this.$tokenIndex];
+ };
+ this.stepForward = function() {
+ var rowCount = this.$session.getLength();
+ this.$tokenIndex += 1;
+
+ while (this.$tokenIndex >= this.$rowTokens.length) {
+ this.$row += 1;
+ if (this.$row >= rowCount) {
+ this.$row = rowCount - 1;
+ return null;
+ }
+
+ this.$rowTokens = this.$session.getTokens(this.$row);
+ this.$tokenIndex = 0;
+ }
+
+ return this.$rowTokens[this.$tokenIndex];
+ };
+ this.getCurrentToken = function () {
+ return this.$rowTokens[this.$tokenIndex];
+ };
+ this.getCurrentTokenRow = function () {
+ return this.$row;
+ };
+ this.getCurrentTokenColumn = function() {
+ var rowTokens = this.$rowTokens;
+ var tokenIndex = this.$tokenIndex;
+
+ // If a column was cached by EditSession.getTokenAt, then use it
+ var column = rowTokens[tokenIndex].start;
+ if (column !== undefined)
+ return column;
+
+ column = 0;
+ while (tokenIndex > 0) {
+ tokenIndex -= 1;
+ column += rowTokens[tokenIndex].value.length;
+ }
+
+ return column;
+ };
+
+}).call(TokenIterator.prototype);
+
+exports.TokenIterator = TokenIterator;
+});
+
+ace.define('ace/edit_session/bracket_match', ['require', 'exports', 'module' , 'ace/token_iterator', 'ace/range'], function(require, exports, module) {
+
+
+var TokenIterator = require("../token_iterator").TokenIterator;
+var Range = require("../range").Range;
+
+
+function BracketMatch() {
+
+ this.findMatchingBracket = function(position) {
+ if (position.column == 0) return null;
+
+ var charBeforeCursor = this.getLine(position.row).charAt(position.column-1);
+ if (charBeforeCursor == "") return null;
+
+ var match = charBeforeCursor.match(/([\(\[\{])|([\)\]\}])/);
+ if (!match)
+ return null;
+
+ if (match[1])
+ return this.$findClosingBracket(match[1], position);
+ else
+ return this.$findOpeningBracket(match[2], position);
+ };
+
+ this.getBracketRange = function(pos) {
+ var line = this.getLine(pos.row);
+ var before = true, range;
+
+ var chr = line.charAt(pos.column-1);
+ var match = chr && chr.match(/([\(\[\{])|([\)\]\}])/);
+ if (!match) {
+ chr = line.charAt(pos.column);
+ pos = {row: pos.row, column: pos.column + 1};
+ match = chr && chr.match(/([\(\[\{])|([\)\]\}])/);
+ before = false;
+ }
+ if (!match)
+ return null;
+
+ if (match[1]) {
+ var bracketPos = this.$findClosingBracket(match[1], pos);
+ if (!bracketPos)
+ return null;
+ range = Range.fromPoints(pos, bracketPos);
+ if (!before) {
+ range.end.column++;
+ range.start.column--;
+ }
+ range.cursor = range.end;
+ } else {
+ var bracketPos = this.$findOpeningBracket(match[2], pos);
+ if (!bracketPos)
+ return null;
+ range = Range.fromPoints(bracketPos, pos);
+ if (!before) {
+ range.start.column++;
+ range.end.column--;
+ }
+ range.cursor = range.start;
+ }
+
+ return range;
+ };
+
+ this.$brackets = {
+ ")": "(",
+ "(": ")",
+ "]": "[",
+ "[": "]",
+ "{": "}",
+ "}": "{"
+ };
+
+ this.$findOpeningBracket = function(bracket, position, typeRe) {
+ var openBracket = this.$brackets[bracket];
+ var depth = 1;
+
+ var iterator = new TokenIterator(this, position.row, position.column);
+ var token = iterator.getCurrentToken();
+ if (!token)
+ token = iterator.stepForward();
+ if (!token)
+ return;
+
+ if (!typeRe){
+ typeRe = new RegExp(
+ "(\\.?" +
+ token.type.replace(".", "\\.").replace("rparen", ".paren")
+ + ")+"
+ );
+ }
+
+ // Start searching in token, just before the character at position.column
+ var valueIndex = position.column - iterator.getCurrentTokenColumn() - 2;
+ var value = token.value;
+
+ while (true) {
+
+ while (valueIndex >= 0) {
+ var chr = value.charAt(valueIndex);
+ if (chr == openBracket) {
+ depth -= 1;
+ if (depth == 0) {
+ return {row: iterator.getCurrentTokenRow(),
+ column: valueIndex + iterator.getCurrentTokenColumn()};
+ }
+ }
+ else if (chr == bracket) {
+ depth += 1;
+ }
+ valueIndex -= 1;
+ }
+
+ // Scan backward through the document, looking for the next token
+ // whose type matches typeRe
+ do {
+ token = iterator.stepBackward();
+ } while (token && !typeRe.test(token.type));
+
+ if (token == null)
+ break;
+
+ value = token.value;
+ valueIndex = value.length - 1;
+ }
+
+ return null;
+ };
+
+ this.$findClosingBracket = function(bracket, position, typeRe) {
+ var closingBracket = this.$brackets[bracket];
+ var depth = 1;
+
+ var iterator = new TokenIterator(this, position.row, position.column);
+ var token = iterator.getCurrentToken();
+ if (!token)
+ token = iterator.stepForward();
+ if (!token)
+ return;
+
+ if (!typeRe){
+ typeRe = new RegExp(
+ "(\\.?" +
+ token.type.replace(".", "\\.").replace("lparen", ".paren")
+ + ")+"
+ );
+ }
+
+ // Start searching in token, after the character at position.column
+ var valueIndex = position.column - iterator.getCurrentTokenColumn();
+
+ while (true) {
+
+ var value = token.value;
+ var valueLength = value.length;
+ while (valueIndex < valueLength) {
+ var chr = value.charAt(valueIndex);
+ if (chr == closingBracket) {
+ depth -= 1;
+ if (depth == 0) {
+ return {row: iterator.getCurrentTokenRow(),
+ column: valueIndex + iterator.getCurrentTokenColumn()};
+ }
+ }
+ else if (chr == bracket) {
+ depth += 1;
+ }
+ valueIndex += 1;
+ }
+
+ // Scan forward through the document, looking for the next token
+ // whose type matches typeRe
+ do {
+ token = iterator.stepForward();
+ } while (token && !typeRe.test(token.type));
+
+ if (token == null)
+ break;
+
+ valueIndex = 0;
+ }
+
+ return null;
+ };
+}
+exports.BracketMatch = BracketMatch;
+
+});
+
+ace.define('ace/search', ['require', 'exports', 'module' , 'ace/lib/lang', 'ace/lib/oop', 'ace/range'], function(require, exports, module) {
+
+
+var lang = require("./lib/lang");
+var oop = require("./lib/oop");
+var Range = require("./range").Range;
+
+/**
+ * new Search()
+ *
+ * Creates a new `Search` object. The following search options are avaliable:
+ *
+ * * `needle`: The string or regular expression you're looking for
+ * * `backwards`: Whether to search backwards from where cursor currently is. Defaults to `false`.
+ * * `wrap`: Whether to wrap the search back to the beginning when it hits the end. Defaults to `false`.
+ * * `caseSensitive`: Whether the search ought to be case-sensitive. Defaults to `false`.
+ * * `wholeWord`: Whether the search matches only on whole words. Defaults to `false`.
+ * * `range`: The [[Range]] to search within. Set this to `null` for the whole document
+ * * `regExp`: Whether the search is a regular expression or not. Defaults to `false`.
+ * * `start`: The starting [[Range]] or cursor position to begin the search
+ * * `skipCurrent`: Whether or not to include the current line in the search. Default to `false`.
+ *
+**/
+
+var Search = function() {
+ this.$options = {};
+};
+
+(function() {
+ /**
+ * Search.set(options) -> Search
+ * - options (Object): An object containing all the new search properties
+ *
+ * Sets the search options via the `options` parameter.
+ *
+ **/
+ this.set = function(options) {
+ oop.mixin(this.$options, options);
+ return this;
+ };
+ this.getOptions = function() {
+ return lang.copyObject(this.$options);
+ };
+
+ this.setOptions = function(options) {
+ this.$options = options;
+ };
+ this.find = function(session) {
+ var iterator = this.$matchIterator(session, this.$options);
+
+ if (!iterator)
+ return false;
+
+ var firstRange = null;
+ iterator.forEach(function(range, row, offset) {
+ if (!range.start) {
+ var column = range.offset + (offset || 0);
+ firstRange = new Range(row, column, row, column+range.length);
+ } else
+ firstRange = range;
+ return true;
+ });
+
+ return firstRange;
+ };
+ this.findAll = function(session) {
+ var options = this.$options;
+ if (!options.needle)
+ return [];
+ this.$assembleRegExp(options);
+
+ var range = options.range;
+ var lines = range
+ ? session.getLines(range.start.row, range.end.row)
+ : session.doc.getAllLines();
+
+ var ranges = [];
+ var re = options.re;
+ if (options.$isMultiLine) {
+ var len = re.length;
+ var maxRow = lines.length - len;
+ for (var row = re.offset || 0; row <= maxRow; row++) {
+ for (var j = 0; j < len; j++)
+ if (lines[row + j].search(re[j]) == -1)
+ break;
+
+ var startLine = lines[row];
+ var line = lines[row + len - 1];
+ var startIndex = startLine.match(re[0])[0].length;
+ var endIndex = line.match(re[len - 1])[0].length;
+
+ ranges.push(new Range(
+ row, startLine.length - startIndex,
+ row + len - 1, endIndex
+ ));
+ }
+ } else {
+ for (var i = 0; i < lines.length; i++) {
+ var matches = lang.getMatchOffsets(lines[i], re);
+ for (var j = 0; j < matches.length; j++) {
+ var match = matches[j];
+ ranges.push(new Range(i, match.offset, i, match.offset + match.length));
+ }
+ }
+ }
+
+ if (range) {
+ var startColumn = range.start.column;
+ var endColumn = range.start.column;
+ var i = 0, j = ranges.length - 1;
+ while (i < j && ranges[i].start.column < startColumn && ranges[i].start.row == range.start.row)
+ i++;
+
+ while (i < j && ranges[j].end.column > endColumn && ranges[j].end.row == range.end.row)
+ j--;
+ return ranges.slice(i, j + 1);
+ }
+
+ return ranges;
+ };
+ this.replace = function(input, replacement) {
+ var options = this.$options;
+
+ var re = this.$assembleRegExp(options);
+ if (options.$isMultiLine)
+ return replacement;
+
+ if (!re)
+ return;
+
+ var match = re.exec(input);
+ if (!match || match[0].length != input.length)
+ return null;
+
+ replacement = input.replace(re, replacement);
+ if (options.preserveCase) {
+ replacement = replacement.split("");
+ for (var i = Math.min(input.length, input.length); i--; ) {
+ var ch = input[i];
+ if (ch && ch.toLowerCase() != ch)
+ replacement[i] = replacement[i].toUpperCase();
+ else
+ replacement[i] = replacement[i].toLowerCase();
+ }
+ replacement = replacement.join("");
+ }
+
+ return replacement;
+ };
+ this.$matchIterator = function(session, options) {
+ var re = this.$assembleRegExp(options);
+ if (!re)
+ return false;
+
+ var self = this, callback, backwards = options.backwards;
+
+ if (options.$isMultiLine) {
+ var len = re.length;
+ var matchIterator = function(line, row, offset) {
+ var startIndex = line.search(re[0]);
+ if (startIndex == -1)
+ return;
+ for (var i = 1; i < len; i++) {
+ line = session.getLine(row + i);
+ if (line.search(re[i]) == -1)
+ return;
+ }
+
+ var endIndex = line.match(re[len - 1])[0].length;
+
+ var range = new Range(row, startIndex, row + len - 1, endIndex);
+ if (re.offset == 1) {
+ range.start.row--;
+ range.start.column = Number.MAX_VALUE;
+ } else if (offset)
+ range.start.column += offset;
+
+ if (callback(range))
+ return true;
+ };
+ } else if (backwards) {
+ var matchIterator = function(line, row, startIndex) {
+ var matches = lang.getMatchOffsets(line, re);
+ for (var i = matches.length-1; i >= 0; i--)
+ if (callback(matches[i], row, startIndex))
+ return true;
+ };
+ } else {
+ var matchIterator = function(line, row, startIndex) {
+ var matches = lang.getMatchOffsets(line, re);
+ for (var i = 0; i < matches.length; i++)
+ if (callback(matches[i], row, startIndex))
+ return true;
+ };
+ }
+
+ return {
+ forEach: function(_callback) {
+ callback = _callback;
+ self.$lineIterator(session, options).forEach(matchIterator);
+ }
+ };
+ };
+
+ this.$assembleRegExp = function(options) {
+ if (options.needle instanceof RegExp)
+ return options.re = options.needle;
+
+ var needle = options.needle;
+
+ if (!options.needle)
+ return options.re = false;
+
+ if (!options.regExp)
+ needle = lang.escapeRegExp(needle);
+
+ if (options.wholeWord)
+ needle = "\\b" + needle + "\\b";
+
+ var modifier = options.caseSensitive ? "g" : "gi";
+
+ options.$isMultiLine = /[\n\r]/.test(needle);
+ if (options.$isMultiLine)
+ return options.re = this.$assembleMultilineRegExp(needle, modifier);
+
+ try {
+ var re = new RegExp(needle, modifier);
+ } catch(e) {
+ re = false;
+ }
+ return options.re = re;
+ };
+
+ this.$assembleMultilineRegExp = function(needle, modifier) {
+ var parts = needle.replace(/\r\n|\r|\n/g, "$\n^").split("\n");
+ var re = [];
+ for (var i = 0; i < parts.length; i++) try {
+ re.push(new RegExp(parts[i], modifier));
+ } catch(e) {
+ return false;
+ }
+ if (parts[0] == "") {
+ re.shift();
+ re.offset = 1;
+ } else {
+ re.offset = 0;
+ }
+ return re;
+ };
+
+ this.$lineIterator = function(session, options) {
+ var backwards = options.backwards == true;
+ var skipCurrent = options.skipCurrent != false;
+
+ var range = options.range;
+ var start = options.start;
+ if (!start)
+ start = range ? range[backwards ? "end" : "start"] : session.selection.getRange();
+
+ if (start.start)
+ start = start[skipCurrent != backwards ? "end" : "start"];
+
+ var firstRow = range ? range.start.row : 0;
+ var lastRow = range ? range.end.row : session.getLength() - 1;
+
+ var forEach = backwards ? function(callback) {
+ var row = start.row;
+
+ var line = session.getLine(row).substring(0, start.column);
+ if (callback(line, row))
+ return;
+
+ for (row--; row >= firstRow; row--)
+ if (callback(session.getLine(row), row))
+ return;
+
+ if (options.wrap == false)
+ return;
+
+ for (row = lastRow, firstRow = start.row; row >= firstRow; row--)
+ if (callback(session.getLine(row), row))
+ return;
+ } : function(callback) {
+ var row = start.row;
+
+ var line = session.getLine(row).substr(start.column);
+ if (callback(line, row, start.column))
+ return;
+
+ for (row = row+1; row <= lastRow; row++)
+ if (callback(session.getLine(row), row))
+ return;
+
+ if (options.wrap == false)
+ return;
+
+ for (row = firstRow, lastRow = start.row; row <= lastRow; row++)
+ if (callback(session.getLine(row), row))
+ return;
+ };
+
+ return {forEach: forEach};
+ };
+
+}).call(Search.prototype);
+
+exports.Search = Search;
+});
+ace.define('ace/commands/command_manager', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/keyboard/hash_handler', 'ace/lib/event_emitter'], function(require, exports, module) {
+
+
+var oop = require("../lib/oop");
+var HashHandler = require("../keyboard/hash_handler").HashHandler;
+var EventEmitter = require("../lib/event_emitter").EventEmitter;
+
+/**
+ * new CommandManager(platform, commands)
+ * - platform (String): Identifier for the platform; must be either `'mac'` or `'win'`
+ * - commands (Array): A list of commands
+ *
+ * TODO
+ *
+ *
+ **/
+
+var CommandManager = function(platform, commands) {
+ this.platform = platform;
+ this.commands = this.byName = {};
+ this.commmandKeyBinding = {};
+
+ this.addCommands(commands);
+
+ this.setDefaultHandler("exec", function(e) {
+ return e.command.exec(e.editor, e.args || {});
+ });
+};
+
+oop.inherits(CommandManager, HashHandler);
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.exec = function(command, editor, args) {
+ if (typeof command === 'string')
+ command = this.commands[command];
+
+ if (!command)
+ return false;
+
+ if (editor && editor.$readOnly && !command.readOnly)
+ return false;
+
+ var retvalue = this._emit("exec", {
+ editor: editor,
+ command: command,
+ args: args
+ });
+
+ return retvalue === false ? false : true;
+ };
+
+ this.toggleRecording = function(editor) {
+ if (this.$inReplay)
+ return;
+
+ editor && editor._emit("changeStatus");
+ if (this.recording) {
+ this.macro.pop();
+ this.removeEventListener("exec", this.$addCommandToMacro);
+
+ if (!this.macro.length)
+ this.macro = this.oldMacro;
+
+ return this.recording = false;
+ }
+ if (!this.$addCommandToMacro) {
+ this.$addCommandToMacro = function(e) {
+ this.macro.push([e.command, e.args]);
+ }.bind(this);
+ }
+
+ this.oldMacro = this.macro;
+ this.macro = [];
+ this.on("exec", this.$addCommandToMacro);
+ return this.recording = true;
+ };
+
+ this.replay = function(editor) {
+ if (this.$inReplay || !this.macro)
+ return;
+
+ if (this.recording)
+ return this.toggleRecording(editor);
+
+ try {
+ this.$inReplay = true;
+ this.macro.forEach(function(x) {
+ if (typeof x == "string")
+ this.exec(x, editor);
+ else
+ this.exec(x[0], editor, x[1]);
+ }, this);
+ } finally {
+ this.$inReplay = false;
+ }
+ };
+
+ this.trimMacro = function(m) {
+ return m.map(function(x){
+ if (typeof x[0] != "string")
+ x[0] = x[0].name;
+ if (!x[1])
+ x = x[0];
+ return x;
+ });
+ };
+
+}).call(CommandManager.prototype);
+
+exports.CommandManager = CommandManager;
+
+});
+
+ace.define('ace/keyboard/hash_handler', ['require', 'exports', 'module' , 'ace/lib/keys'], function(require, exports, module) {
+
+
+var keyUtil = require("../lib/keys");
+
+function HashHandler(config, platform) {
+ this.platform = platform;
+ this.commands = {};
+ this.commmandKeyBinding = {};
+
+ this.addCommands(config);
+};
+
+(function() {
+
+ this.addCommand = function(command) {
+ if (this.commands[command.name])
+ this.removeCommand(command);
+
+ this.commands[command.name] = command;
+
+ if (command.bindKey)
+ this._buildKeyHash(command);
+ };
+
+ this.removeCommand = function(command) {
+ var name = (typeof command === 'string' ? command : command.name);
+ command = this.commands[name];
+ delete this.commands[name];
+
+ // exhaustive search is brute force but since removeCommand is
+ // not a performance critical operation this should be OK
+ var ckb = this.commmandKeyBinding;
+ for (var hashId in ckb) {
+ for (var key in ckb[hashId]) {
+ if (ckb[hashId][key] == command)
+ delete ckb[hashId][key];
+ }
+ }
+ };
+
+ this.bindKey = function(key, command) {
+ if(!key)
+ return;
+ if (typeof command == "function") {
+ this.addCommand({exec: command, bindKey: key, name: key});
+ return;
+ }
+
+ var ckb = this.commmandKeyBinding;
+ key.split("|").forEach(function(keyPart) {
+ var binding = this.parseKeys(keyPart, command);
+ var hashId = binding.hashId;
+ (ckb[hashId] || (ckb[hashId] = {}))[binding.key] = command;
+ }, this);
+ };
+
+ this.addCommands = function(commands) {
+ commands && Object.keys(commands).forEach(function(name) {
+ var command = commands[name];
+ if (typeof command === "string")
+ return this.bindKey(command, name);
+
+ if (typeof command === "function")
+ command = { exec: command };
+
+ if (!command.name)
+ command.name = name;
+
+ this.addCommand(command);
+ }, this);
+ };
+
+ this.removeCommands = function(commands) {
+ Object.keys(commands).forEach(function(name) {
+ this.removeCommand(commands[name]);
+ }, this);
+ };
+
+ this.bindKeys = function(keyList) {
+ Object.keys(keyList).forEach(function(key) {
+ this.bindKey(key, keyList[key]);
+ }, this);
+ };
+
+ this._buildKeyHash = function(command) {
+ var binding = command.bindKey;
+ if (!binding)
+ return;
+
+ var key = typeof binding == "string" ? binding: binding[this.platform];
+ this.bindKey(key, command);
+ };
+
+ // accepts keys in the form ctrl+Enter or ctrl-Enter
+ // keys without modifiers or shift only
+ this.parseKeys = function(keys) {
+ var parts = keys.toLowerCase().split(/[\-\+]([\-\+])?/).filter(function(x){return x});
+ var key = parts.pop();
+
+ var keyCode = keyUtil[key];
+ if (keyUtil.FUNCTION_KEYS[keyCode])
+ key = keyUtil.FUNCTION_KEYS[keyCode].toLowerCase();
+ else if (!parts.length)
+ return {key: key, hashId: -1};
+ else if (parts.length == 1 && parts[0] == "shift")
+ return {key: key.toUpperCase(), hashId: -1};
+
+ var hashId = 0;
+ for (var i = parts.length; i--;) {
+ var modifier = keyUtil.KEY_MODS[parts[i]];
+ if (modifier == null)
+ throw "invalid modifier " + parts[i] + " in " + keys;
+ hashId |= modifier;
+ }
+ return {key: key, hashId: hashId};
+ };
+
+ this.findKeyCommand = function findKeyCommand(hashId, keyString) {
+ var ckbr = this.commmandKeyBinding;
+ return ckbr[hashId] && ckbr[hashId][keyString];
+ };
+
+ this.handleKeyboard = function(data, hashId, keyString, keyCode) {
+ return {
+ command: this.findKeyCommand(hashId, keyString)
+ };
+ };
+
+}).call(HashHandler.prototype)
+
+exports.HashHandler = HashHandler;
+});
+
+ace.define('ace/commands/default_commands', ['require', 'exports', 'module' , 'ace/lib/lang'], function(require, exports, module) {
+
+
+var lang = require("../lib/lang");
+
+function bindKey(win, mac) {
+ return {
+ win: win,
+ mac: mac
+ };
+}
+
+exports.commands = [{
+ name: "selectall",
+ bindKey: bindKey("Ctrl-A", "Command-A"),
+ exec: function(editor) { editor.selectAll(); },
+ readOnly: true
+}, {
+ name: "centerselection",
+ bindKey: bindKey(null, "Ctrl-L"),
+ exec: function(editor) { editor.centerSelection(); },
+ readOnly: true
+}, {
+ name: "gotoline",
+ bindKey: bindKey("Ctrl-L", "Command-L"),
+ exec: function(editor) {
+ var line = parseInt(prompt("Enter line number:"), 10);
+ if (!isNaN(line)) {
+ editor.gotoLine(line);
+ }
+ },
+ readOnly: true
+}, {
+ name: "fold",
+ bindKey: bindKey("Alt-L|Ctrl-F1", "Command-Alt-L|Command-F1"),
+ exec: function(editor) { editor.session.toggleFold(false); },
+ readOnly: true
+}, {
+ name: "unfold",
+ bindKey: bindKey("Alt-Shift-L|Ctrl-Shift-F1", "Command-Alt-Shift-L|Command-Shift-F1"),
+ exec: function(editor) { editor.session.toggleFold(true); },
+ readOnly: true
+}, {
+ name: "foldall",
+ bindKey: bindKey("Alt-0", "Command-Option-0"),
+ exec: function(editor) { editor.session.foldAll(); },
+ readOnly: true
+}, {
+ name: "unfoldall",
+ bindKey: bindKey("Alt-Shift-0", "Command-Option-Shift-0"),
+ exec: function(editor) { editor.session.unfold(); },
+ readOnly: true
+}, {
+ name: "findnext",
+ bindKey: bindKey("Ctrl-K", "Command-G"),
+ exec: function(editor) { editor.findNext(); },
+ readOnly: true
+}, {
+ name: "findprevious",
+ bindKey: bindKey("Ctrl-Shift-K", "Command-Shift-G"),
+ exec: function(editor) { editor.findPrevious(); },
+ readOnly: true
+}, {
+ name: "find",
+ bindKey: bindKey("Ctrl-F", "Command-F"),
+ exec: function(editor) {
+ var needle = prompt("Find:", editor.getCopyText());
+ editor.find(needle);
+ },
+ readOnly: true
+}, {
+ name: "overwrite",
+ bindKey: "Insert",
+ exec: function(editor) { editor.toggleOverwrite(); },
+ readOnly: true
+}, {
+ name: "selecttostart",
+ bindKey: bindKey("Ctrl-Shift-Home", "Command-Shift-Up"),
+ exec: function(editor) { editor.getSelection().selectFileStart(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "gotostart",
+ bindKey: bindKey("Ctrl-Home", "Command-Home|Command-Up"),
+ exec: function(editor) { editor.navigateFileStart(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selectup",
+ bindKey: bindKey("Shift-Up", "Shift-Up"),
+ exec: function(editor) { editor.getSelection().selectUp(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "golineup",
+ bindKey: bindKey("Up", "Up|Ctrl-P"),
+ exec: function(editor, args) { editor.navigateUp(args.times); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selecttoend",
+ bindKey: bindKey("Ctrl-Shift-End", "Command-Shift-Down"),
+ exec: function(editor) { editor.getSelection().selectFileEnd(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "gotoend",
+ bindKey: bindKey("Ctrl-End", "Command-End|Command-Down"),
+ exec: function(editor) { editor.navigateFileEnd(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selectdown",
+ bindKey: bindKey("Shift-Down", "Shift-Down"),
+ exec: function(editor) { editor.getSelection().selectDown(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "golinedown",
+ bindKey: bindKey("Down", "Down|Ctrl-N"),
+ exec: function(editor, args) { editor.navigateDown(args.times); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selectwordleft",
+ bindKey: bindKey("Ctrl-Shift-Left", "Option-Shift-Left"),
+ exec: function(editor) { editor.getSelection().selectWordLeft(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "gotowordleft",
+ bindKey: bindKey("Ctrl-Left", "Option-Left"),
+ exec: function(editor) { editor.navigateWordLeft(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selecttolinestart",
+ bindKey: bindKey("Alt-Shift-Left", "Command-Shift-Left"),
+ exec: function(editor) { editor.getSelection().selectLineStart(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "gotolinestart",
+ bindKey: bindKey("Alt-Left|Home", "Command-Left|Home|Ctrl-A"),
+ exec: function(editor) { editor.navigateLineStart(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selectleft",
+ bindKey: bindKey("Shift-Left", "Shift-Left"),
+ exec: function(editor) { editor.getSelection().selectLeft(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "gotoleft",
+ bindKey: bindKey("Left", "Left|Ctrl-B"),
+ exec: function(editor, args) { editor.navigateLeft(args.times); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selectwordright",
+ bindKey: bindKey("Ctrl-Shift-Right", "Option-Shift-Right"),
+ exec: function(editor) { editor.getSelection().selectWordRight(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "gotowordright",
+ bindKey: bindKey("Ctrl-Right", "Option-Right"),
+ exec: function(editor) { editor.navigateWordRight(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selecttolineend",
+ bindKey: bindKey("Alt-Shift-Right", "Command-Shift-Right"),
+ exec: function(editor) { editor.getSelection().selectLineEnd(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "gotolineend",
+ bindKey: bindKey("Alt-Right|End", "Command-Right|End|Ctrl-E"),
+ exec: function(editor) { editor.navigateLineEnd(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selectright",
+ bindKey: bindKey("Shift-Right", "Shift-Right"),
+ exec: function(editor) { editor.getSelection().selectRight(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "gotoright",
+ bindKey: bindKey("Right", "Right|Ctrl-F"),
+ exec: function(editor, args) { editor.navigateRight(args.times); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selectpagedown",
+ bindKey: "Shift-PageDown",
+ exec: function(editor) { editor.selectPageDown(); },
+ readOnly: true
+}, {
+ name: "pagedown",
+ bindKey: bindKey(null, "Option-PageDown"),
+ exec: function(editor) { editor.scrollPageDown(); },
+ readOnly: true
+}, {
+ name: "gotopagedown",
+ bindKey: bindKey("PageDown", "PageDown|Ctrl-V"),
+ exec: function(editor) { editor.gotoPageDown(); },
+ readOnly: true
+}, {
+ name: "selectpageup",
+ bindKey: "Shift-PageUp",
+ exec: function(editor) { editor.selectPageUp(); },
+ readOnly: true
+}, {
+ name: "pageup",
+ bindKey: bindKey(null, "Option-PageUp"),
+ exec: function(editor) { editor.scrollPageUp(); },
+ readOnly: true
+}, {
+ name: "gotopageup",
+ bindKey: "PageUp",
+ exec: function(editor) { editor.gotoPageUp(); },
+ readOnly: true
+}, {
+ name: "scrollup",
+ bindKey: bindKey("Ctrl-Up", null),
+ exec: function(e) { e.renderer.scrollBy(0, -2 * e.renderer.layerConfig.lineHeight); },
+ readOnly: true
+}, {
+ name: "scrolldown",
+ bindKey: bindKey("Ctrl-Down", null),
+ exec: function(e) { e.renderer.scrollBy(0, 2 * e.renderer.layerConfig.lineHeight); },
+ readOnly: true
+}, {
+ name: "selectlinestart",
+ bindKey: "Shift-Home",
+ exec: function(editor) { editor.getSelection().selectLineStart(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selectlineend",
+ bindKey: "Shift-End",
+ exec: function(editor) { editor.getSelection().selectLineEnd(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "togglerecording",
+ bindKey: bindKey("Ctrl-Alt-E", "Command-Option-E"),
+ exec: function(editor) { editor.commands.toggleRecording(editor); },
+ readOnly: true
+}, {
+ name: "replaymacro",
+ bindKey: bindKey("Ctrl-Shift-E", "Command-Shift-E"),
+ exec: function(editor) { editor.commands.replay(editor); },
+ readOnly: true
+}, {
+ name: "jumptomatching",
+ bindKey: bindKey("Ctrl-P", "Ctrl-Shift-P"),
+ exec: function(editor) { editor.jumpToMatching(); },
+ multiSelectAction: "forEach",
+ readOnly: true
+}, {
+ name: "selecttomatching",
+ bindKey: bindKey("Ctrl-Shift-P", null),
+ exec: function(editor) { editor.jumpToMatching(true); },
+ readOnly: true
+},
+
+// commands disabled in readOnly mode
+{
+ name: "cut",
+ exec: function(editor) {
+ var range = editor.getSelectionRange();
+ editor._emit("cut", range);
+
+ if (!editor.selection.isEmpty()) {
+ editor.session.remove(range);
+ editor.clearSelection();
+ }
+ },
+ multiSelectAction: "forEach"
+}, {
+ name: "removeline",
+ bindKey: bindKey("Ctrl-D", "Command-D"),
+ exec: function(editor) { editor.removeLines(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "duplicateSelection",
+ bindKey: bindKey("Ctrl-Shift-D", "Command-Shift-D"),
+ exec: function(editor) { editor.duplicateSelection(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "togglecomment",
+ bindKey: bindKey("Ctrl-/", "Command-/"),
+ exec: function(editor) { editor.toggleCommentLines(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "replace",
+ bindKey: bindKey("Ctrl-R", "Command-Option-F"),
+ exec: function(editor) {
+ var needle = prompt("Find:", editor.getCopyText());
+ if (!needle)
+ return;
+ var replacement = prompt("Replacement:");
+ if (!replacement)
+ return;
+ editor.replace(replacement, {needle: needle});
+ }
+}, {
+ name: "replaceall",
+ bindKey: bindKey("Ctrl-Shift-R", "Command-Shift-Option-F"),
+ exec: function(editor) {
+ var needle = prompt("Find:");
+ if (!needle)
+ return;
+ var replacement = prompt("Replacement:");
+ if (!replacement)
+ return;
+ editor.replaceAll(replacement, {needle: needle});
+ }
+}, {
+ name: "undo",
+ bindKey: bindKey("Ctrl-Z", "Command-Z"),
+ exec: function(editor) { editor.undo(); }
+}, {
+ name: "redo",
+ bindKey: bindKey("Ctrl-Shift-Z|Ctrl-Y", "Command-Shift-Z|Command-Y"),
+ exec: function(editor) { editor.redo(); }
+}, {
+ name: "copylinesup",
+ bindKey: bindKey("Alt-Shift-Up", "Command-Option-Up"),
+ exec: function(editor) { editor.copyLinesUp(); }
+}, {
+ name: "movelinesup",
+ bindKey: bindKey("Alt-Up", "Option-Up"),
+ exec: function(editor) { editor.moveLinesUp(); }
+}, {
+ name: "copylinesdown",
+ bindKey: bindKey("Alt-Shift-Down", "Command-Option-Down"),
+ exec: function(editor) { editor.copyLinesDown(); }
+}, {
+ name: "movelinesdown",
+ bindKey: bindKey("Alt-Down", "Option-Down"),
+ exec: function(editor) { editor.moveLinesDown(); }
+}, {
+ name: "del",
+ bindKey: bindKey("Delete", "Delete|Ctrl-D"),
+ exec: function(editor) { editor.remove("right"); },
+ multiSelectAction: "forEach"
+}, {
+ name: "backspace",
+ bindKey: bindKey(
+ "Command-Backspace|Option-Backspace|Shift-Backspace|Backspace",
+ "Ctrl-Backspace|Command-Backspace|Shift-Backspace|Backspace|Ctrl-H"
+ ),
+ exec: function(editor) { editor.remove("left"); },
+ multiSelectAction: "forEach"
+}, {
+ name: "removetolinestart",
+ bindKey: bindKey("Alt-Backspace", "Command-Backspace"),
+ exec: function(editor) { editor.removeToLineStart(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "removetolineend",
+ bindKey: bindKey("Alt-Delete", "Ctrl-K"),
+ exec: function(editor) { editor.removeToLineEnd(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "removewordleft",
+ bindKey: bindKey("Ctrl-Backspace", "Alt-Backspace|Ctrl-Alt-Backspace"),
+ exec: function(editor) { editor.removeWordLeft(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "removewordright",
+ bindKey: bindKey("Ctrl-Delete", "Alt-Delete"),
+ exec: function(editor) { editor.removeWordRight(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "outdent",
+ bindKey: bindKey("Shift-Tab", "Shift-Tab"),
+ exec: function(editor) { editor.blockOutdent(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "indent",
+ bindKey: bindKey("Tab", "Tab"),
+ exec: function(editor) { editor.indent(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "insertstring",
+ exec: function(editor, str) { editor.insert(str); },
+ multiSelectAction: "forEach"
+}, {
+ name: "inserttext",
+ exec: function(editor, args) {
+ editor.insert(lang.stringRepeat(args.text || "", args.times || 1));
+ },
+ multiSelectAction: "forEach"
+}, {
+ name: "splitline",
+ bindKey: bindKey(null, "Ctrl-O"),
+ exec: function(editor) { editor.splitLine(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "transposeletters",
+ bindKey: bindKey("Ctrl-T", "Ctrl-T"),
+ exec: function(editor) { editor.transposeLetters(); },
+ multiSelectAction: function(editor) {editor.transposeSelections(1); }
+}, {
+ name: "touppercase",
+ bindKey: bindKey("Ctrl-U", "Ctrl-U"),
+ exec: function(editor) { editor.toUpperCase(); },
+ multiSelectAction: "forEach"
+}, {
+ name: "tolowercase",
+ bindKey: bindKey("Ctrl-Shift-U", "Ctrl-Shift-U"),
+ exec: function(editor) { editor.toLowerCase(); },
+ multiSelectAction: "forEach"
+}];
+
+});
+
+ace.define('ace/undomanager', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+/**
+ * class UndoManager
+ *
+ * This object maintains the undo stack for an [[EditSession `EditSession`]].
+ *
+ **/
+
+/**
+ * new UndoManager()
+ *
+ * Resets the current undo state and creates a new `UndoManager`.
+ **/
+var UndoManager = function() {
+ this.reset();
+};
+
+(function() {
+
+ /**
+ * UndoManager.execute(options) -> Void
+ * - options (Object): Contains additional properties
+ *
+ * Provides a means for implementing your own undo manager. `options` has one property, `args`, an [[Array `Array`]], with two elements:
+ *
+ * * `args[0]` is an array of deltas
+ * * `args[1]` is the document to associate with
+ *
+ **/
+ this.execute = function(options) {
+ var deltas = options.args[0];
+ this.$doc = options.args[1];
+ this.$undoStack.push(deltas);
+ this.$redoStack = [];
+ };
+ this.undo = function(dontSelect) {
+ var deltas = this.$undoStack.pop();
+ var undoSelectionRange = null;
+ if (deltas) {
+ undoSelectionRange =
+ this.$doc.undoChanges(deltas, dontSelect);
+ this.$redoStack.push(deltas);
+ }
+ return undoSelectionRange;
+ };
+ this.redo = function(dontSelect) {
+ var deltas = this.$redoStack.pop();
+ var redoSelectionRange = null;
+ if (deltas) {
+ redoSelectionRange =
+ this.$doc.redoChanges(deltas, dontSelect);
+ this.$undoStack.push(deltas);
+ }
+ return redoSelectionRange;
+ };
+ this.reset = function() {
+ this.$undoStack = [];
+ this.$redoStack = [];
+ };
+ this.hasUndo = function() {
+ return this.$undoStack.length > 0;
+ };
+ this.hasRedo = function() {
+ return this.$redoStack.length > 0;
+ };
+
+}).call(UndoManager.prototype);
+
+exports.UndoManager = UndoManager;
+});
+
+ace.define('ace/virtual_renderer', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/dom', 'ace/lib/event', 'ace/lib/useragent', 'ace/config', 'ace/lib/net', 'ace/layer/gutter', 'ace/layer/marker', 'ace/layer/text', 'ace/layer/cursor', 'ace/scrollbar', 'ace/renderloop', 'ace/lib/event_emitter', 'text!ace/css/editor.css'], function(require, exports, module) {
+
+
+var oop = require("./lib/oop");
+var dom = require("./lib/dom");
+var event = require("./lib/event");
+var useragent = require("./lib/useragent");
+var config = require("./config");
+var net = require("./lib/net");
+var GutterLayer = require("./layer/gutter").Gutter;
+var MarkerLayer = require("./layer/marker").Marker;
+var TextLayer = require("./layer/text").Text;
+var CursorLayer = require("./layer/cursor").Cursor;
+var ScrollBar = require("./scrollbar").ScrollBar;
+var RenderLoop = require("./renderloop").RenderLoop;
+var EventEmitter = require("./lib/event_emitter").EventEmitter;
+var editorCss = require("text!./css/editor.css");
+
+dom.importCssString(editorCss, "ace_editor");
+
+/**
+ * new VirtualRenderer(container, theme)
+ * - container (DOMElement): The root element of the editor
+ * - theme (String): The starting theme
+ *
+ * Constructs a new `VirtualRenderer` within the `container` specified, applying the given `theme`.
+ *
+ **/
+
+var VirtualRenderer = function(container, theme) {
+ var _self = this;
+
+ this.container = container;
+
+ // TODO: this breaks rendering in Cloud9 with multiple ace instances
+// // Imports CSS once per DOM document ('ace_editor' serves as an identifier).
+// dom.importCssString(editorCss, "ace_editor", container.ownerDocument);
+
+ // in IE <= 9 the native cursor always shines through
+ this.$keepTextAreaAtCursor = !useragent.isIE;
+
+ dom.addCssClass(container, "ace_editor");
+
+ this.setTheme(theme);
+
+ this.$gutter = dom.createElement("div");
+ this.$gutter.className = "ace_gutter";
+ this.container.appendChild(this.$gutter);
+
+ this.scroller = dom.createElement("div");
+ this.scroller.className = "ace_scroller";
+ this.container.appendChild(this.scroller);
+
+ this.content = dom.createElement("div");
+ this.content.className = "ace_content";
+ this.scroller.appendChild(this.content);
+
+ this.setHighlightGutterLine(true);
+ this.$gutterLayer = new GutterLayer(this.$gutter);
+ this.$gutterLayer.on("changeGutterWidth", this.onResize.bind(this, true));
+
+ this.$markerBack = new MarkerLayer(this.content);
+
+ var textLayer = this.$textLayer = new TextLayer(this.content);
+ this.canvas = textLayer.element;
+
+ this.$markerFront = new MarkerLayer(this.content);
+
+ this.characterWidth = textLayer.getCharacterWidth();
+ this.lineHeight = textLayer.getLineHeight();
+
+ this.$cursorLayer = new CursorLayer(this.content);
+ this.$cursorPadding = 8;
+
+ // Indicates whether the horizontal scrollbar is visible
+ this.$horizScroll = false;
+ this.$horizScrollAlwaysVisible = false;
+
+ this.$animatedScroll = false;
+
+ this.scrollBar = new ScrollBar(container);
+ this.scrollBar.addEventListener("scroll", function(e) {
+ if (!_self.$inScrollAnimation)
+ _self.session.setScrollTop(e.data);
+ });
+
+ this.scrollTop = 0;
+ this.scrollLeft = 0;
+
+ event.addListener(this.scroller, "scroll", function() {
+ var scrollLeft = _self.scroller.scrollLeft;
+ _self.scrollLeft = scrollLeft;
+ _self.session.setScrollLeft(scrollLeft);
+ });
+
+ this.cursorPos = {
+ row : 0,
+ column : 0
+ };
+
+ this.$textLayer.addEventListener("changeCharacterSize", function() {
+ _self.characterWidth = textLayer.getCharacterWidth();
+ _self.lineHeight = textLayer.getLineHeight();
+ _self.$updatePrintMargin();
+ _self.onResize(true);
+
+ _self.$loop.schedule(_self.CHANGE_FULL);
+ });
+
+ this.$size = {
+ width: 0,
+ height: 0,
+ scrollerHeight: 0,
+ scrollerWidth: 0
+ };
+
+ this.layerConfig = {
+ width : 1,
+ padding : 0,
+ firstRow : 0,
+ firstRowScreen: 0,
+ lastRow : 0,
+ lineHeight : 1,
+ characterWidth : 1,
+ minHeight : 1,
+ maxHeight : 1,
+ offset : 0,
+ height : 1
+ };
+
+ this.$loop = new RenderLoop(
+ this.$renderChanges.bind(this),
+ this.container.ownerDocument.defaultView
+ );
+ this.$loop.schedule(this.CHANGE_FULL);
+
+ this.setPadding(4);
+ this.$updatePrintMargin();
+};
+
+(function() {
+ this.showGutter = true;
+
+ this.CHANGE_CURSOR = 1;
+ this.CHANGE_MARKER = 2;
+ this.CHANGE_GUTTER = 4;
+ this.CHANGE_SCROLL = 8;
+ this.CHANGE_LINES = 16;
+ this.CHANGE_TEXT = 32;
+ this.CHANGE_SIZE = 64;
+ this.CHANGE_MARKER_BACK = 128;
+ this.CHANGE_MARKER_FRONT = 256;
+ this.CHANGE_FULL = 512;
+ this.CHANGE_H_SCROLL = 1024;
+
+ oop.implement(this, EventEmitter);
+ this.setSession = function(session) {
+ this.session = session;
+
+ this.scroller.className = "ace_scroller";
+
+ this.$cursorLayer.setSession(session);
+ this.$markerBack.setSession(session);
+ this.$markerFront.setSession(session);
+ this.$gutterLayer.setSession(session);
+ this.$textLayer.setSession(session);
+ this.$loop.schedule(this.CHANGE_FULL);
+
+ };
+ this.updateLines = function(firstRow, lastRow) {
+ if (lastRow === undefined)
+ lastRow = Infinity;
+
+ if (!this.$changedLines) {
+ this.$changedLines = {
+ firstRow: firstRow,
+ lastRow: lastRow
+ };
+ }
+ else {
+ if (this.$changedLines.firstRow > firstRow)
+ this.$changedLines.firstRow = firstRow;
+
+ if (this.$changedLines.lastRow < lastRow)
+ this.$changedLines.lastRow = lastRow;
+ }
+
+ this.$loop.schedule(this.CHANGE_LINES);
+ };
+
+ this.onChangeTabSize = function() {
+ this.$loop.schedule(this.CHANGE_TEXT | this.CHANGE_MARKER);
+ this.$textLayer.onChangeTabSize();
+ };
+ this.updateText = function() {
+ this.$loop.schedule(this.CHANGE_TEXT);
+ };
+ this.updateFull = function(force) {
+ if (force){
+ this.$renderChanges(this.CHANGE_FULL, true);
+ }
+ else {
+ this.$loop.schedule(this.CHANGE_FULL);
+ }
+ };
+ this.updateFontSize = function() {
+ this.$textLayer.checkForSizeChanges();
+ };
+ this.onResize = function(force, gutterWidth, width, height) {
+ var changes = this.CHANGE_SIZE;
+ var size = this.$size;
+
+ if (this.resizing > 2)
+ return;
+ else if (this.resizing > 1)
+ this.resizing++;
+ else
+ this.resizing = force ? 1 : 0;
+
+ if (!height)
+ height = dom.getInnerHeight(this.container);
+ if (force || size.height != height) {
+ size.height = height;
+
+ this.scroller.style.height = height + "px";
+ size.scrollerHeight = this.scroller.clientHeight;
+ this.scrollBar.setHeight(size.scrollerHeight);
+
+ if (this.session) {
+ this.session.setScrollTop(this.getScrollTop());
+ changes = changes | this.CHANGE_FULL;
+ }
+ }
+
+ if (!width)
+ width = dom.getInnerWidth(this.container);
+ if (force || this.resizing > 1 || size.width != width) {
+ size.width = width;
+
+ var gutterWidth = this.showGutter ? this.$gutter.offsetWidth : 0;
+ this.scroller.style.left = gutterWidth + "px";
+ size.scrollerWidth = Math.max(0, width - gutterWidth - this.scrollBar.getWidth());
+ this.scroller.style.right = this.scrollBar.getWidth() + "px";
+
+ if (this.session.getUseWrapMode() && this.adjustWrapLimit() || force)
+ changes = changes | this.CHANGE_FULL;
+ }
+
+ if (force)
+ this.$renderChanges(changes, true);
+ else
+ this.$loop.schedule(changes);
+
+ if (force)
+ delete this.resizing;
+ };
+ this.adjustWrapLimit = function() {
+ var availableWidth = this.$size.scrollerWidth - this.$padding * 2;
+ var limit = Math.floor(availableWidth / this.characterWidth);
+ return this.session.adjustWrapLimit(limit);
+ };
+ this.setAnimatedScroll = function(shouldAnimate){
+ this.$animatedScroll = shouldAnimate;
+ };
+ this.getAnimatedScroll = function() {
+ return this.$animatedScroll;
+ };
+ this.setShowInvisibles = function(showInvisibles) {
+ if (this.$textLayer.setShowInvisibles(showInvisibles))
+ this.$loop.schedule(this.CHANGE_TEXT);
+ };
+ this.getShowInvisibles = function() {
+ return this.$textLayer.showInvisibles;
+ };
+
+ this.getDisplayIndentGuides = function() {
+ return this.$textLayer.displayIndentGuides;
+ };
+
+ this.setDisplayIndentGuides = function(display) {
+ if (this.$textLayer.setDisplayIndentGuides(display))
+ this.$loop.schedule(this.CHANGE_TEXT);
+ };
+
+ this.$showPrintMargin = true;
+ this.setShowPrintMargin = function(showPrintMargin) {
+ this.$showPrintMargin = showPrintMargin;
+ this.$updatePrintMargin();
+ };
+ this.getShowPrintMargin = function() {
+ return this.$showPrintMargin;
+ };
+
+ this.$printMarginColumn = 80;
+ this.setPrintMarginColumn = function(showPrintMargin) {
+ this.$printMarginColumn = showPrintMargin;
+ this.$updatePrintMargin();
+ };
+ this.getPrintMarginColumn = function() {
+ return this.$printMarginColumn;
+ };
+ this.getShowGutter = function(){
+ return this.showGutter;
+ };
+ this.setShowGutter = function(show){
+ if(this.showGutter === show)
+ return;
+ this.$gutter.style.display = show ? "block" : "none";
+ this.showGutter = show;
+ this.onResize(true);
+ };
+
+ this.getFadeFoldWidgets = function(){
+ return dom.hasCssClass(this.$gutter, "ace_fade-fold-widgets");
+ };
+
+ this.setFadeFoldWidgets = function(show) {
+ if (show)
+ dom.addCssClass(this.$gutter, "ace_fade-fold-widgets");
+ else
+ dom.removeCssClass(this.$gutter, "ace_fade-fold-widgets");
+ };
+
+ this.$highlightGutterLine = false;
+ this.setHighlightGutterLine = function(shouldHighlight) {
+ if (this.$highlightGutterLine == shouldHighlight)
+ return;
+ this.$highlightGutterLine = shouldHighlight;
+
+ if (!this.$gutterLineHighlight) {
+ this.$gutterLineHighlight = dom.createElement("div");
+ this.$gutterLineHighlight.className = "ace_gutter_active_line";
+ this.$gutter.appendChild(this.$gutterLineHighlight);
+ return;
+ }
+
+ this.$gutterLineHighlight.style.display = shouldHighlight ? "" : "none";
+ // if cursorlayer have never been updated there's nothing on screen to update
+ if (this.$cursorLayer.$pixelPos)
+ this.$updateGutterLineHighlight();
+ };
+
+ this.getHighlightGutterLine = function() {
+ return this.$highlightGutterLine;
+ };
+
+ this.$updateGutterLineHighlight = function() {
+ this.$gutterLineHighlight.style.top = this.$cursorLayer.$pixelPos.top - this.layerConfig.offset + "px";
+ this.$gutterLineHighlight.style.height = this.layerConfig.lineHeight + "px";
+ };
+
+ this.$updatePrintMargin = function() {
+ var containerEl;
+
+ if (!this.$showPrintMargin && !this.$printMarginEl)
+ return;
+
+ if (!this.$printMarginEl) {
+ containerEl = dom.createElement("div");
+ containerEl.className = "ace_print_margin_layer";
+ this.$printMarginEl = dom.createElement("div");
+ this.$printMarginEl.className = "ace_print_margin";
+ containerEl.appendChild(this.$printMarginEl);
+ this.content.insertBefore(containerEl, this.$textLayer.element);
+ }
+
+ var style = this.$printMarginEl.style;
+ style.left = ((this.characterWidth * this.$printMarginColumn) + this.$padding) + "px";
+ style.visibility = this.$showPrintMargin ? "visible" : "hidden";
+ };
+ this.getContainerElement = function() {
+ return this.container;
+ };
+ this.getMouseEventTarget = function() {
+ return this.content;
+ };
+ this.getTextAreaContainer = function() {
+ return this.container;
+ };
+
+ // move text input over the cursor
+ // this is required for iOS and IME
+ this.$moveTextAreaToCursor = function() {
+ if (!this.$keepTextAreaAtCursor)
+ return;
+
+ var posTop = this.$cursorLayer.$pixelPos.top;
+ var posLeft = this.$cursorLayer.$pixelPos.left;
+ posTop -= this.layerConfig.offset;
+
+ if (posTop < 0 || posTop > this.layerConfig.height - this.lineHeight)
+ return;
+
+ var w = this.characterWidth;
+ if (this.$composition)
+ w += this.textarea.scrollWidth;
+ posLeft -= this.scrollLeft;
+ if (posLeft > this.$size.scrollerWidth - w)
+ posLeft = this.$size.scrollerWidth - w;
+
+ if (this.showGutter)
+ posLeft += this.$gutterLayer.gutterWidth;
+
+ this.textarea.style.height = this.lineHeight + "px";
+ this.textarea.style.width = w + "px";
+ this.textarea.style.left = posLeft + "px";
+ this.textarea.style.top = posTop - 1 + "px";
+ };
+ this.getFirstVisibleRow = function() {
+ return this.layerConfig.firstRow;
+ };
+ this.getFirstFullyVisibleRow = function() {
+ return this.layerConfig.firstRow + (this.layerConfig.offset === 0 ? 0 : 1);
+ };
+ this.getLastFullyVisibleRow = function() {
+ var flint = Math.floor((this.layerConfig.height + this.layerConfig.offset) / this.layerConfig.lineHeight);
+ return this.layerConfig.firstRow - 1 + flint;
+ };
+ this.getLastVisibleRow = function() {
+ return this.layerConfig.lastRow;
+ };
+
+ this.$padding = null;
+ this.setPadding = function(padding) {
+ this.$padding = padding;
+ this.$textLayer.setPadding(padding);
+ this.$cursorLayer.setPadding(padding);
+ this.$markerFront.setPadding(padding);
+ this.$markerBack.setPadding(padding);
+ this.$loop.schedule(this.CHANGE_FULL);
+ this.$updatePrintMargin();
+ };
+ this.getHScrollBarAlwaysVisible = function() {
+ return this.$horizScrollAlwaysVisible;
+ };
+ this.setHScrollBarAlwaysVisible = function(alwaysVisible) {
+ if (this.$horizScrollAlwaysVisible != alwaysVisible) {
+ this.$horizScrollAlwaysVisible = alwaysVisible;
+ if (!this.$horizScrollAlwaysVisible || !this.$horizScroll)
+ this.$loop.schedule(this.CHANGE_SCROLL);
+ }
+ };
+
+ this.$updateScrollBar = function() {
+ this.scrollBar.setInnerHeight(this.layerConfig.maxHeight);
+ this.scrollBar.setScrollTop(this.scrollTop);
+ };
+
+ this.$renderChanges = function(changes, force) {
+ if (!force && (!changes || !this.session || !this.container.offsetWidth))
+ return;
+
+ // text, scrolling and resize changes can cause the view port size to change
+ if (changes & this.CHANGE_FULL ||
+ changes & this.CHANGE_SIZE ||
+ changes & this.CHANGE_TEXT ||
+ changes & this.CHANGE_LINES ||
+ changes & this.CHANGE_SCROLL
+ )
+ this.$computeLayerConfig();
+
+ // horizontal scrolling
+ if (changes & this.CHANGE_H_SCROLL) {
+ this.scroller.scrollLeft = this.scrollLeft;
+
+ // read the value after writing it since the value might get clipped
+ var scrollLeft = this.scroller.scrollLeft;
+ this.scrollLeft = scrollLeft;
+ this.session.setScrollLeft(scrollLeft);
+
+ this.scroller.className = this.scrollLeft == 0 ? "ace_scroller" : "ace_scroller horscroll";
+ }
+
+ // full
+ if (changes & this.CHANGE_FULL) {
+ this.$textLayer.checkForSizeChanges();
+ // update scrollbar first to not lose scroll position when gutter calls resize
+ this.$updateScrollBar();
+ this.$textLayer.update(this.layerConfig);
+ if (this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ this.$markerBack.update(this.layerConfig);
+ this.$markerFront.update(this.layerConfig);
+ this.$cursorLayer.update(this.layerConfig);
+ this.$moveTextAreaToCursor();
+ this.$highlightGutterLine && this.$updateGutterLineHighlight();
+ return;
+ }
+
+ // scrolling
+ if (changes & this.CHANGE_SCROLL) {
+ this.$updateScrollBar();
+ if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES)
+ this.$textLayer.update(this.layerConfig);
+ else
+ this.$textLayer.scrollLines(this.layerConfig);
+
+ if (this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ this.$markerBack.update(this.layerConfig);
+ this.$markerFront.update(this.layerConfig);
+ this.$cursorLayer.update(this.layerConfig);
+ this.$moveTextAreaToCursor();
+ this.$highlightGutterLine && this.$updateGutterLineHighlight();
+ return;
+ }
+
+ if (changes & this.CHANGE_TEXT) {
+ this.$textLayer.update(this.layerConfig);
+ if (this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ }
+ else if (changes & this.CHANGE_LINES) {
+ if (this.$updateLines() || (changes & this.CHANGE_GUTTER) && this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ }
+ else if (changes & this.CHANGE_TEXT || changes & this.CHANGE_GUTTER) {
+ if (this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ }
+
+ if (changes & this.CHANGE_CURSOR) {
+ this.$cursorLayer.update(this.layerConfig);
+ this.$moveTextAreaToCursor();
+ this.$highlightGutterLine && this.$updateGutterLineHighlight();
+ }
+
+ if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) {
+ this.$markerFront.update(this.layerConfig);
+ }
+
+ if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_BACK)) {
+ this.$markerBack.update(this.layerConfig);
+ }
+
+ if (changes & this.CHANGE_SIZE)
+ this.$updateScrollBar();
+ };
+
+ this.$computeLayerConfig = function() {
+ var session = this.session;
+
+ var offset = this.scrollTop % this.lineHeight;
+ var minHeight = this.$size.scrollerHeight + this.lineHeight;
+
+ var longestLine = this.$getLongestLine();
+
+ var horizScroll = this.$horizScrollAlwaysVisible || this.$size.scrollerWidth - longestLine < 0;
+ var horizScrollChanged = this.$horizScroll !== horizScroll;
+ this.$horizScroll = horizScroll;
+ if (horizScrollChanged) {
+ this.scroller.style.overflowX = horizScroll ? "scroll" : "hidden";
+ // when we hide scrollbar scroll event isn't emited
+ // leaving session with wrong scrollLeft value
+ if (!horizScroll)
+ this.session.setScrollLeft(0);
+ }
+ var maxHeight = this.session.getScreenLength() * this.lineHeight;
+ this.session.setScrollTop(Math.max(0, Math.min(this.scrollTop, maxHeight - this.$size.scrollerHeight)));
+
+ var lineCount = Math.ceil(minHeight / this.lineHeight) - 1;
+ var firstRow = Math.max(0, Math.round((this.scrollTop - offset) / this.lineHeight));
+ var lastRow = firstRow + lineCount;
+
+ // Map lines on the screen to lines in the document.
+ var firstRowScreen, firstRowHeight;
+ var lineHeight = this.lineHeight;
+ firstRow = session.screenToDocumentRow(firstRow, 0);
+
+ // Check if firstRow is inside of a foldLine. If true, then use the first
+ // row of the foldLine.
+ var foldLine = session.getFoldLine(firstRow);
+ if (foldLine) {
+ firstRow = foldLine.start.row;
+ }
+
+ firstRowScreen = session.documentToScreenRow(firstRow, 0);
+ firstRowHeight = session.getRowLength(firstRow) * lineHeight;
+
+ lastRow = Math.min(session.screenToDocumentRow(lastRow, 0), session.getLength() - 1);
+ minHeight = this.$size.scrollerHeight + session.getRowLength(lastRow) * lineHeight +
+ firstRowHeight;
+
+ offset = this.scrollTop - firstRowScreen * lineHeight;
+
+ this.layerConfig = {
+ width : longestLine,
+ padding : this.$padding,
+ firstRow : firstRow,
+ firstRowScreen: firstRowScreen,
+ lastRow : lastRow,
+ lineHeight : lineHeight,
+ characterWidth : this.characterWidth,
+ minHeight : minHeight,
+ maxHeight : maxHeight,
+ offset : offset,
+ height : this.$size.scrollerHeight
+ };
+
+ // For debugging.
+ // console.log(JSON.stringify(this.layerConfig));
+
+ this.$gutterLayer.element.style.marginTop = (-offset) + "px";
+ this.content.style.marginTop = (-offset) + "px";
+ this.content.style.width = longestLine + 2 * this.$padding + "px";
+ this.content.style.height = minHeight + "px";
+
+ // Horizontal scrollbar visibility may have changed, which changes
+ // the client height of the scroller
+ if (horizScrollChanged)
+ this.onResize(true);
+ };
+
+ this.$updateLines = function() {
+ var firstRow = this.$changedLines.firstRow;
+ var lastRow = this.$changedLines.lastRow;
+ this.$changedLines = null;
+
+ var layerConfig = this.layerConfig;
+
+ if (firstRow > layerConfig.lastRow + 1) { return; }
+ if (lastRow < layerConfig.firstRow) { return; }
+
+ // if the last row is unknown -> redraw everything
+ if (lastRow === Infinity) {
+ if (this.showGutter)
+ this.$gutterLayer.update(layerConfig);
+ this.$textLayer.update(layerConfig);
+ return;
+ }
+
+ // else update only the changed rows
+ this.$textLayer.updateLines(layerConfig, firstRow, lastRow);
+ return true;
+ };
+
+ this.$getLongestLine = function() {
+ var charCount = this.session.getScreenWidth();
+ if (this.$textLayer.showInvisibles)
+ charCount += 1;
+
+ return Math.max(this.$size.scrollerWidth - 2 * this.$padding, Math.round(charCount * this.characterWidth));
+ };
+ this.updateFrontMarkers = function() {
+ this.$markerFront.setMarkers(this.session.getMarkers(true));
+ this.$loop.schedule(this.CHANGE_MARKER_FRONT);
+ };
+ this.updateBackMarkers = function() {
+ this.$markerBack.setMarkers(this.session.getMarkers());
+ this.$loop.schedule(this.CHANGE_MARKER_BACK);
+ };
+ this.addGutterDecoration = function(row, className){
+ this.$gutterLayer.addGutterDecoration(row, className);
+ };
+ this.removeGutterDecoration = function(row, className){
+ this.$gutterLayer.removeGutterDecoration(row, className);
+ };
+ this.updateBreakpoints = function(rows) {
+ this.$loop.schedule(this.CHANGE_GUTTER);
+ };
+ this.setAnnotations = function(annotations) {
+ this.$gutterLayer.setAnnotations(annotations);
+ this.$loop.schedule(this.CHANGE_GUTTER);
+ };
+ this.updateCursor = function() {
+ this.$loop.schedule(this.CHANGE_CURSOR);
+ };
+ this.hideCursor = function() {
+ this.$cursorLayer.hideCursor();
+ };
+ this.showCursor = function() {
+ this.$cursorLayer.showCursor();
+ };
+
+ this.scrollSelectionIntoView = function(anchor, lead, offset) {
+ // first scroll anchor into view then scroll lead into view
+ this.scrollCursorIntoView(anchor, offset);
+ this.scrollCursorIntoView(lead, offset);
+ };
+ this.scrollCursorIntoView = function(cursor, offset) {
+ // the editor is not visible
+ if (this.$size.scrollerHeight === 0)
+ return;
+
+ var pos = this.$cursorLayer.getPixelPosition(cursor);
+
+ var left = pos.left;
+ var top = pos.top;
+
+ if (this.scrollTop > top) {
+ if (offset)
+ top -= offset * this.$size.scrollerHeight;
+ this.session.setScrollTop(top);
+ } else if (this.scrollTop + this.$size.scrollerHeight < top + this.lineHeight) {
+ if (offset)
+ top += offset * this.$size.scrollerHeight;
+ this.session.setScrollTop(top + this.lineHeight - this.$size.scrollerHeight);
+ }
+
+ var scrollLeft = this.scrollLeft;
+
+ if (scrollLeft > left) {
+ if (left < this.$padding + 2 * this.layerConfig.characterWidth)
+ left = 0;
+ this.session.setScrollLeft(left);
+ } else if (scrollLeft + this.$size.scrollerWidth < left + this.characterWidth) {
+ this.session.setScrollLeft(Math.round(left + this.characterWidth - this.$size.scrollerWidth));
+ }
+ };
+ this.getScrollTop = function() {
+ return this.session.getScrollTop();
+ };
+ this.getScrollLeft = function() {
+ return this.session.getScrollLeft();
+ };
+ this.getScrollTopRow = function() {
+ return this.scrollTop / this.lineHeight;
+ };
+ this.getScrollBottomRow = function() {
+ return Math.max(0, Math.floor((this.scrollTop + this.$size.scrollerHeight) / this.lineHeight) - 1);
+ };
+ this.scrollToRow = function(row) {
+ this.session.setScrollTop(row * this.lineHeight);
+ };
+
+ this.alignCursor = function(cursor, alignment) {
+ if (typeof cursor == "number")
+ cursor = {row: cursor, column: 0};
+
+ var pos = this.$cursorLayer.getPixelPosition(cursor);
+ var h = this.$size.scrollerHeight - this.lineHeight;
+ var offset = pos.top - h * (alignment || 0);
+
+ this.session.setScrollTop(offset);
+ return offset;
+ };
+
+ this.STEPS = 8;
+ this.$calcSteps = function(fromValue, toValue){
+ var i = 0;
+ var l = this.STEPS;
+ var steps = [];
+
+ var func = function(t, x_min, dx) {
+ return dx * (Math.pow(t - 1, 3) + 1) + x_min;
+ };
+
+ for (i = 0; i < l; ++i)
+ steps.push(func(i / this.STEPS, fromValue, toValue - fromValue));
+
+ return steps;
+ };
+ this.scrollToLine = function(line, center, animate, callback) {
+ var pos = this.$cursorLayer.getPixelPosition({row: line, column: 0});
+ var offset = pos.top;
+ if (center)
+ offset -= this.$size.scrollerHeight / 2;
+
+ var initialScroll = this.scrollTop;
+ this.session.setScrollTop(offset);
+ if (animate !== false)
+ this.animateScrolling(initialScroll, callback);
+ };
+
+ this.animateScrolling = function(fromValue, callback) {
+ var toValue = this.scrollTop;
+ if (this.$animatedScroll && Math.abs(fromValue - toValue) < 100000) {
+ var _self = this;
+ var steps = _self.$calcSteps(fromValue, toValue);
+ this.$inScrollAnimation = true;
+
+ clearInterval(this.$timer);
+
+ _self.session.setScrollTop(steps.shift());
+ this.$timer = setInterval(function() {
+ if (steps.length) {
+ _self.session.setScrollTop(steps.shift());
+ // trick session to think it's already scrolled to not loose toValue
+ _self.session.$scrollTop = toValue;
+ } else if (toValue != null) {
+ _self.session.$scrollTop = -1;
+ _self.session.setScrollTop(toValue);
+ toValue = null;
+ } else {
+ // do this on separate step to not get spurious scroll event from scrollbar
+ _self.$timer = clearInterval(_self.$timer);
+ _self.$inScrollAnimation = false;
+ callback && callback();
+ }
+ }, 10);
+ }
+ };
+ this.scrollToY = function(scrollTop) {
+ // after calling scrollBar.setScrollTop
+ // scrollbar sends us event with same scrollTop. ignore it
+ if (this.scrollTop !== scrollTop) {
+ this.$loop.schedule(this.CHANGE_SCROLL);
+ this.scrollTop = scrollTop;
+ }
+ };
+ this.scrollToX = function(scrollLeft) {
+ if (scrollLeft < 0)
+ scrollLeft = 0;
+
+ if (this.scrollLeft !== scrollLeft)
+ this.scrollLeft = scrollLeft;
+ this.$loop.schedule(this.CHANGE_H_SCROLL);
+ };
+ this.scrollBy = function(deltaX, deltaY) {
+ deltaY && this.session.setScrollTop(this.session.getScrollTop() + deltaY);
+ deltaX && this.session.setScrollLeft(this.session.getScrollLeft() + deltaX);
+ };
+ this.isScrollableBy = function(deltaX, deltaY) {
+ if (deltaY < 0 && this.session.getScrollTop() > 0)
+ return true;
+ if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight < this.layerConfig.maxHeight)
+ return true;
+ // todo: handle horizontal scrolling
+ };
+
+ this.pixelToScreenCoordinates = function(x, y) {
+ var canvasPos = this.scroller.getBoundingClientRect();
+
+ var offset = (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth;
+ var row = Math.floor((y + this.scrollTop - canvasPos.top) / this.lineHeight);
+ var col = Math.round(offset);
+
+ return {row: row, column: col, side: offset - col > 0 ? 1 : -1};
+ };
+
+ this.screenToTextCoordinates = function(x, y) {
+ var canvasPos = this.scroller.getBoundingClientRect();
+
+ var col = Math.round(
+ (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth
+ );
+ var row = Math.floor(
+ (y + this.scrollTop - canvasPos.top) / this.lineHeight
+ );
+
+ return this.session.screenToDocumentPosition(row, Math.max(col, 0));
+ };
+ this.textToScreenCoordinates = function(row, column) {
+ var canvasPos = this.scroller.getBoundingClientRect();
+ var pos = this.session.documentToScreenPosition(row, column);
+
+ var x = this.$padding + Math.round(pos.column * this.characterWidth);
+ var y = pos.row * this.lineHeight;
+
+ return {
+ pageX: canvasPos.left + x - this.scrollLeft,
+ pageY: canvasPos.top + y - this.scrollTop
+ };
+ };
+ this.visualizeFocus = function() {
+ dom.addCssClass(this.container, "ace_focus");
+ };
+ this.visualizeBlur = function() {
+ dom.removeCssClass(this.container, "ace_focus");
+ };
+ this.showComposition = function(position) {
+ if (!this.$composition)
+ this.$composition = {
+ keepTextAreaAtCursor: this.$keepTextAreaAtCursor,
+ cssText: this.textarea.style.cssText
+ };
+
+ this.$keepTextAreaAtCursor = true;
+ dom.addCssClass(this.textarea, "ace_composition");
+ this.textarea.style.cssText = "";
+ this.$moveTextAreaToCursor();
+ };
+ this.setCompositionText = function(text) {
+ this.$moveTextAreaToCursor();
+ };
+ this.hideComposition = function() {
+ if (!this.$composition)
+ return;
+
+ dom.removeCssClass(this.textarea, "ace_composition");
+ this.$keepTextAreaAtCursor = this.$composition.keepTextAreaAtCursor;
+ this.textarea.style.cssText = this.$composition.cssText;
+ this.$composition = null;
+ };
+
+ this._loadTheme = function(name, callback) {
+ if (!config.get("packaged"))
+ return callback();
+
+ net.loadScript(config.moduleUrl(name, "theme"), callback);
+ };
+ this.setTheme = function(theme) {
+ var _self = this;
+
+ this.$themeValue = theme;
+ if (!theme || typeof theme == "string") {
+ var moduleName = theme || "ace/theme/textmate";
+
+ var module;
+ try {
+ module = require(moduleName);
+ } catch (e) {};
+ if (module)
+ return afterLoad(module);
+
+ _self._loadTheme(moduleName, function() {
+ require([moduleName], function(module) {
+ if (_self.$themeValue !== theme)
+ return;
+
+ afterLoad(module);
+ });
+ });
+ } else {
+ afterLoad(theme);
+ }
+
+ function afterLoad(theme) {
+ dom.importCssString(
+ theme.cssText,
+ theme.cssClass,
+ _self.container.ownerDocument
+ );
+
+ if (_self.$theme)
+ dom.removeCssClass(_self.container, _self.$theme);
+
+ _self.$theme = theme ? theme.cssClass : null;
+
+ if (_self.$theme)
+ dom.addCssClass(_self.container, _self.$theme);
+
+ if (theme && theme.isDark)
+ dom.addCssClass(_self.container, "ace_dark");
+ else
+ dom.removeCssClass(_self.container, "ace_dark");
+
+ // force re-measure of the gutter width
+ if (_self.$size) {
+ _self.$size.width = 0;
+ _self.onResize();
+ }
+ }
+ };
+ this.getTheme = function() {
+ return this.$themeValue;
+ };
+
+ // Methods allows to add / remove CSS classnames to the editor element.
+ // This feature can be used by plug-ins to provide a visual indication of
+ // a certain mode that editor is in.
+
+ /**
+ * VirtualRenderer.setStyle(style)
+ * - style (String): A class name
+ *
+ * [Adds a new class, `style`, to the editor.]{: #VirtualRenderer.setStyle}
+ **/
+ this.setStyle = function setStyle(style) {
+ dom.addCssClass(this.container, style);
+ };
+ this.unsetStyle = function unsetStyle(style) {
+ dom.removeCssClass(this.container, style);
+ };
+ this.destroy = function() {
+ this.$textLayer.destroy();
+ this.$cursorLayer.destroy();
+ };
+
+}).call(VirtualRenderer.prototype);
+
+exports.VirtualRenderer = VirtualRenderer;
+});
+
+ace.define('ace/layer/gutter', ['require', 'exports', 'module' , 'ace/lib/dom', 'ace/lib/oop', 'ace/lib/event_emitter'], function(require, exports, module) {
+
+
+var dom = require("../lib/dom");
+var oop = require("../lib/oop");
+var EventEmitter = require("../lib/event_emitter").EventEmitter;
+
+var Gutter = function(parentEl) {
+ this.element = dom.createElement("div");
+ this.element.className = "ace_layer ace_gutter-layer";
+ parentEl.appendChild(this.element);
+ this.setShowFoldWidgets(this.$showFoldWidgets);
+
+ this.gutterWidth = 0;
+
+ this.$annotations = [];
+};
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.setSession = function(session) {
+ this.session = session;
+ };
+
+ this.addGutterDecoration = function(row, className){
+ if (window.console)
+ console.warn && console.warn("deprecated use session.addGutterDecoration");
+ this.session.addGutterDecoration(row, className);
+ };
+
+ this.removeGutterDecoration = function(row, className){
+ if (window.console)
+ console.warn && console.warn("deprecated use session.removeGutterDecoration");
+ this.session.removeGutterDecoration(row, className);
+ };
+
+ this.setAnnotations = function(annotations) {
+ // iterate over sparse array
+ this.$annotations = [];
+ for (var row in annotations) if (annotations.hasOwnProperty(row)) {
+ var rowAnnotations = annotations[row];
+ if (!rowAnnotations)
+ continue;
+
+ var rowInfo = this.$annotations[row] = {
+ text: []
+ };
+ for (var i=0; i foldStart) {
+ i = fold.end.row + 1;
+ fold = this.session.getNextFoldLine(i, fold);
+ foldStart = fold ?fold.start.row :Infinity;
+ }
+ if(i > lastRow)
+ break;
+
+ var annotation = this.$annotations[i] || emptyAnno;
+ html.push(
+ "
",
+ lastLineNumber = i + 1
+ );
+
+ if (foldWidgets) {
+ var c = foldWidgets[i];
+ // check if cached value is invalidated and we need to recompute
+ if (c == null)
+ c = foldWidgets[i] = this.session.getFoldWidget(i);
+ if (c)
+ html.push(
+ ""
+ );
+ }
+
+ html.push("
");
+
+ i++;
+ }
+
+ this.element = dom.setInnerHtml(this.element, html.join(""));
+ this.element.style.height = config.minHeight + "px";
+
+ if (this.session.$useWrapMode)
+ lastLineNumber = this.session.getLength();
+
+ var gutterWidth = ("" + lastLineNumber).length * config.characterWidth;
+ var padding = this.$padding || this.$computePadding();
+ gutterWidth += padding.left + padding.right;
+ if (gutterWidth !== this.gutterWidth) {
+ this.gutterWidth = gutterWidth;
+ this.element.style.width = Math.ceil(this.gutterWidth) + "px";
+ this._emit("changeGutterWidth", gutterWidth);
+ }
+ };
+
+ this.$showFoldWidgets = true;
+ this.setShowFoldWidgets = function(show) {
+ if (show)
+ dom.addCssClass(this.element, "ace_folding-enabled");
+ else
+ dom.removeCssClass(this.element, "ace_folding-enabled");
+
+ this.$showFoldWidgets = show;
+ this.$padding = null;
+ };
+
+ this.getShowFoldWidgets = function() {
+ return this.$showFoldWidgets;
+ };
+
+ this.$computePadding = function() {
+ if (!this.element.firstChild)
+ return {left: 0, right: 0};
+ var style = dom.computedStyle(this.element.firstChild);
+ this.$padding = {}
+ this.$padding.left = parseInt(style.paddingLeft) + 1;
+ this.$padding.right = parseInt(style.paddingRight);
+ return this.$padding;
+ };
+
+ this.getRegion = function(point) {
+ var padding = this.$padding || this.$computePadding();
+ var rect = this.element.getBoundingClientRect();
+ if (point.x < padding.left + rect.left)
+ return "markers";
+ if (this.$showFoldWidgets && point.x > rect.right - padding.right)
+ return "foldWidgets";
+ };
+
+}).call(Gutter.prototype);
+
+exports.Gutter = Gutter;
+
+});
+
+ace.define('ace/layer/marker', ['require', 'exports', 'module' , 'ace/range', 'ace/lib/dom'], function(require, exports, module) {
+
+
+var Range = require("../range").Range;
+var dom = require("../lib/dom");
+
+var Marker = function(parentEl) {
+ this.element = dom.createElement("div");
+ this.element.className = "ace_layer ace_marker-layer";
+ parentEl.appendChild(this.element);
+};
+
+(function() {
+
+ this.$padding = 0;
+
+ this.setPadding = function(padding) {
+ this.$padding = padding;
+ };
+ this.setSession = function(session) {
+ this.session = session;
+ };
+
+ this.setMarkers = function(markers) {
+ this.markers = markers;
+ };
+
+ this.update = function(config) {
+ var config = config || this.config;
+ if (!config)
+ return;
+
+ this.config = config;
+
+
+ var html = [];
+ for (var key in this.markers) {
+ var marker = this.markers[key];
+
+ if (!marker.range) {
+ marker.update(html, this, this.session, config);
+ continue;
+ }
+
+ var range = marker.range.clipRows(config.firstRow, config.lastRow);
+ if (range.isEmpty()) continue;
+
+ range = range.toScreenRange(this.session);
+ if (marker.renderer) {
+ var top = this.$getTop(range.start.row, config);
+ var left = Math.round(
+ this.$padding + range.start.column * config.characterWidth
+ );
+ marker.renderer(html, range, left, top, config);
+ }
+ else if (range.isMultiLine()) {
+ if (marker.type == "text") {
+ this.drawTextMarker(html, range, marker.clazz, config);
+ } else {
+ this.drawMultiLineMarker(
+ html, range, marker.clazz, config,
+ marker.type
+ );
+ }
+ }
+ else {
+ this.drawSingleLineMarker(
+ html, range, marker.clazz + " start", config,
+ null, marker.type
+ );
+ }
+ }
+ this.element = dom.setInnerHtml(this.element, html.join(""));
+ };
+
+ this.$getTop = function(row, layerConfig) {
+ return (row - layerConfig.firstRowScreen) * layerConfig.lineHeight;
+ };
+
+ // Draws a marker, which spans a range of text on multiple lines
+ this.drawTextMarker = function(stringBuilder, range, clazz, layerConfig) {
+ // selection start
+ var row = range.start.row;
+
+ var lineRange = new Range(
+ row, range.start.column,
+ row, this.session.getScreenLastRowColumn(row)
+ );
+ this.drawSingleLineMarker(stringBuilder, lineRange, clazz + " start", layerConfig, 1, "text");
+
+ // selection end
+ row = range.end.row;
+ lineRange = new Range(row, 0, row, range.end.column);
+ this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig, 0, "text");
+
+ for (row = range.start.row + 1; row < range.end.row; row++) {
+ lineRange.start.row = row;
+ lineRange.end.row = row;
+ lineRange.end.column = this.session.getScreenLastRowColumn(row);
+ this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig, 1, "text");
+ }
+ };
+
+ // Draws a multi line marker, where lines span the full width
+ this.drawMultiLineMarker = function(stringBuilder, range, clazz, config, type) {
+ var padding = type === "background" ? 0 : this.$padding;
+ // from selection start to the end of the line
+ var height = config.lineHeight;
+ var top = this.$getTop(range.start.row, config);
+ var left = Math.round(padding + range.start.column * config.characterWidth);
+
+ stringBuilder.push(
+ ""
+ );
+
+ // from start of the last line to the selection end
+ top = this.$getTop(range.end.row, config);
+ var width = Math.round(range.end.column * config.characterWidth);
+
+ stringBuilder.push(
+ ""
+ );
+
+ // all the complete lines
+ height = (range.end.row - range.start.row - 1) * config.lineHeight;
+ if (height < 0)
+ return;
+ top = this.$getTop(range.start.row + 1, config);
+
+ stringBuilder.push(
+ ""
+ );
+ };
+
+ // Draws a marker which covers part or whole width of a single screen line
+ this.drawSingleLineMarker = function(stringBuilder, range, clazz, layerConfig, extraLength, type) {
+ var padding = type === "background" ? 0 : this.$padding;
+ var height = layerConfig.lineHeight;
+
+ if (type === "background")
+ var width = layerConfig.width;
+ else
+ width = Math.round((range.end.column + (extraLength || 0) - range.start.column) * layerConfig.characterWidth);
+
+ var top = this.$getTop(range.start.row, layerConfig);
+ var left = Math.round(
+ padding + range.start.column * layerConfig.characterWidth
+ );
+
+ stringBuilder.push(
+ ""
+ );
+ };
+
+}).call(Marker.prototype);
+
+exports.Marker = Marker;
+
+});
+
+ace.define('ace/layer/text', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/dom', 'ace/lib/lang', 'ace/lib/useragent', 'ace/lib/event_emitter'], function(require, exports, module) {
+
+
+var oop = require("../lib/oop");
+var dom = require("../lib/dom");
+var lang = require("../lib/lang");
+var useragent = require("../lib/useragent");
+var EventEmitter = require("../lib/event_emitter").EventEmitter;
+
+var Text = function(parentEl) {
+ this.element = dom.createElement("div");
+ this.element.className = "ace_layer ace_text-layer";
+ parentEl.appendChild(this.element);
+
+ this.$characterSize = this.$measureSizes() || {width: 0, height: 0};
+ this.$pollSizeChanges();
+};
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.EOF_CHAR = "\xB6"; //"¶";
+ this.EOL_CHAR = "\xAC"; //"¬";
+ this.TAB_CHAR = "\u2192"; //"→" "\u21E5";
+ this.SPACE_CHAR = "\xB7"; //"·";
+ this.$padding = 0;
+
+ this.setPadding = function(padding) {
+ this.$padding = padding;
+ this.element.style.padding = "0 " + padding + "px";
+ };
+
+ this.getLineHeight = function() {
+ return this.$characterSize.height || 1;
+ };
+
+ this.getCharacterWidth = function() {
+ return this.$characterSize.width || 1;
+ };
+
+ this.checkForSizeChanges = function() {
+ var size = this.$measureSizes();
+ if (size && (this.$characterSize.width !== size.width || this.$characterSize.height !== size.height)) {
+ this.$characterSize = size;
+ this._emit("changeCharacterSize", {data: size});
+ }
+ };
+
+ this.$pollSizeChanges = function() {
+ var self = this;
+ this.$pollSizeChangesTimer = setInterval(function() {
+ self.checkForSizeChanges();
+ }, 500);
+ };
+
+ this.$fontStyles = {
+ fontFamily : 1,
+ fontSize : 1,
+ fontWeight : 1,
+ fontStyle : 1,
+ lineHeight : 1
+ };
+
+ this.$measureSizes = useragent.isIE || useragent.isOldGecko ? function() {
+ var n = 1000;
+ if (!this.$measureNode) {
+ var measureNode = this.$measureNode = dom.createElement("div");
+ var style = measureNode.style;
+
+ style.width = style.height = "auto";
+ style.left = style.top = (-n * 40) + "px";
+
+ style.visibility = "hidden";
+ style.position = "fixed";
+ style.overflow = "visible";
+ style.whiteSpace = "nowrap";
+
+ // in FF 3.6 monospace fonts can have a fixed sub pixel width.
+ // that's why we have to measure many characters
+ // Note: characterWidth can be a float!
+ measureNode.innerHTML = lang.stringRepeat("Xy", n);
+
+ if (this.element.ownerDocument.body) {
+ this.element.ownerDocument.body.appendChild(measureNode);
+ } else {
+ var container = this.element.parentNode;
+ while (!dom.hasCssClass(container, "ace_editor"))
+ container = container.parentNode;
+ container.appendChild(measureNode);
+ }
+ }
+
+ // Size and width can be null if the editor is not visible or
+ // detached from the document
+ if (!this.element.offsetWidth)
+ return null;
+
+ var style = this.$measureNode.style;
+ var computedStyle = dom.computedStyle(this.element);
+ for (var prop in this.$fontStyles)
+ style[prop] = computedStyle[prop];
+
+ var size = {
+ height: this.$measureNode.offsetHeight,
+ width: this.$measureNode.offsetWidth / (n * 2)
+ };
+
+ // Size and width can be null if the editor is not visible or
+ // detached from the document
+ if (size.width == 0 || size.height == 0)
+ return null;
+
+ return size;
+ }
+ : function() {
+ if (!this.$measureNode) {
+ var measureNode = this.$measureNode = dom.createElement("div");
+ var style = measureNode.style;
+
+ style.width = style.height = "auto";
+ style.left = style.top = -100 + "px";
+
+ style.visibility = "hidden";
+ style.position = "fixed";
+ style.overflow = "visible";
+ style.whiteSpace = "nowrap";
+
+ measureNode.innerHTML = "X";
+
+ var container = this.element.parentNode;
+ while (container && !dom.hasCssClass(container, "ace_editor"))
+ container = container.parentNode;
+
+ if (!container)
+ return this.$measureNode = null;
+
+ container.appendChild(measureNode);
+ }
+
+ var rect = this.$measureNode.getBoundingClientRect();
+
+ var size = {
+ height: rect.height,
+ width: rect.width
+ };
+
+ // Size and width can be null if the editor is not visible or
+ // detached from the document
+ if (size.width == 0 || size.height == 0)
+ return null;
+
+ return size;
+ };
+
+ this.setSession = function(session) {
+ this.session = session;
+ this.$computeTabString();
+ };
+
+ this.showInvisibles = false;
+ this.setShowInvisibles = function(showInvisibles) {
+ if (this.showInvisibles == showInvisibles)
+ return false;
+
+ this.showInvisibles = showInvisibles;
+ this.$computeTabString();
+ return true;
+ };
+
+ this.displayIndentGuides = true;
+ this.setDisplayIndentGuides = function(display) {
+ if (this.displayIndentGuides == display)
+ return false;
+
+ this.displayIndentGuides = display;
+ this.$computeTabString();
+ return true;
+ };
+
+ this.$tabStrings = [];
+ this.onChangeTabSize =
+ this.$computeTabString = function() {
+ var tabSize = this.session.getTabSize();
+ this.tabSize = tabSize;
+ var tabStr = this.$tabStrings = [0];
+ for (var i = 1; i < tabSize + 1; i++) {
+ if (this.showInvisibles) {
+ tabStr.push(""
+ + this.TAB_CHAR
+ + Array(i).join(" ")
+ + "");
+ } else {
+ tabStr.push(new Array(i+1).join(" "));
+ }
+ }
+ if (this.displayIndentGuides) {
+ this.$indentGuideRe = /\s\S| \t|\t |\s$/;
+ var className = "ace_indent-guide";
+ var content = Array(this.tabSize + 1).join(" ");
+ var tabContent = content;
+ if (this.showInvisibles) {
+ className += " ace_invisible";
+ tabContent = this.TAB_CHAR + content.substr(6);
+ }
+
+ this.$tabStrings[" "] = "" + content + "";
+ this.$tabStrings["\t"] = "" + tabContent + "";
+ }
+ };
+
+ this.updateLines = function(config, firstRow, lastRow) {
+ // Due to wrap line changes there can be new lines if e.g.
+ // the line to updated wrapped in the meantime.
+ if (this.config.lastRow != config.lastRow ||
+ this.config.firstRow != config.firstRow) {
+ this.scrollLines(config);
+ }
+ this.config = config;
+
+ var first = Math.max(firstRow, config.firstRow);
+ var last = Math.min(lastRow, config.lastRow);
+
+ var lineElements = this.element.childNodes;
+ var lineElementsIdx = 0;
+
+ for (var row = config.firstRow; row < first; row++) {
+ var foldLine = this.session.getFoldLine(row);
+ if (foldLine) {
+ if (foldLine.containsRow(first)) {
+ first = foldLine.start.row;
+ break;
+ } else {
+ row = foldLine.end.row;
+ }
+ }
+ lineElementsIdx ++;
+ }
+
+ var row = first;
+ var foldLine = this.session.getNextFoldLine(row);
+ var foldStart = foldLine ? foldLine.start.row : Infinity;
+
+ while (true) {
+ if (row > foldStart) {
+ row = foldLine.end.row+1;
+ foldLine = this.session.getNextFoldLine(row, foldLine);
+ foldStart = foldLine ? foldLine.start.row :Infinity;
+ }
+ if (row > last)
+ break;
+
+ var lineElement = lineElements[lineElementsIdx++];
+ if (lineElement) {
+ var html = [];
+ this.$renderLine(
+ html, row, !this.$useLineGroups(), row == foldStart ? foldLine : false
+ );
+ dom.setInnerHtml(lineElement, html.join(""));
+ }
+ row++;
+ }
+ };
+
+ this.scrollLines = function(config) {
+ var oldConfig = this.config;
+ this.config = config;
+
+ if (!oldConfig || oldConfig.lastRow < config.firstRow)
+ return this.update(config);
+
+ if (config.lastRow < oldConfig.firstRow)
+ return this.update(config);
+
+ var el = this.element;
+ if (oldConfig.firstRow < config.firstRow)
+ for (var row=this.session.getFoldedRowCount(oldConfig.firstRow, config.firstRow - 1); row>0; row--)
+ el.removeChild(el.firstChild);
+
+ if (oldConfig.lastRow > config.lastRow)
+ for (var row=this.session.getFoldedRowCount(config.lastRow + 1, oldConfig.lastRow); row>0; row--)
+ el.removeChild(el.lastChild);
+
+ if (config.firstRow < oldConfig.firstRow) {
+ var fragment = this.$renderLinesFragment(config, config.firstRow, oldConfig.firstRow - 1);
+ if (el.firstChild)
+ el.insertBefore(fragment, el.firstChild);
+ else
+ el.appendChild(fragment);
+ }
+
+ if (config.lastRow > oldConfig.lastRow) {
+ var fragment = this.$renderLinesFragment(config, oldConfig.lastRow + 1, config.lastRow);
+ el.appendChild(fragment);
+ }
+ };
+
+ this.$renderLinesFragment = function(config, firstRow, lastRow) {
+ var fragment = this.element.ownerDocument.createDocumentFragment();
+ var row = firstRow;
+ var foldLine = this.session.getNextFoldLine(row);
+ var foldStart = foldLine ? foldLine.start.row : Infinity;
+
+ while (true) {
+ if (row > foldStart) {
+ row = foldLine.end.row+1;
+ foldLine = this.session.getNextFoldLine(row, foldLine);
+ foldStart = foldLine ? foldLine.start.row : Infinity;
+ }
+ if (row > lastRow)
+ break;
+
+ var container = dom.createElement("div");
+
+ var html = [];
+ // Get the tokens per line as there might be some lines in between
+ // beeing folded.
+ this.$renderLine(html, row, false, row == foldStart ? foldLine : false);
+
+ // don't use setInnerHtml since we are working with an empty DIV
+ container.innerHTML = html.join("");
+ if (this.$useLineGroups()) {
+ container.className = 'ace_line_group';
+ fragment.appendChild(container);
+ } else {
+ var lines = container.childNodes
+ while(lines.length)
+ fragment.appendChild(lines[0]);
+ }
+
+ row++;
+ }
+ return fragment;
+ };
+
+ this.update = function(config) {
+ this.config = config;
+
+ var html = [];
+ var firstRow = config.firstRow, lastRow = config.lastRow;
+
+ var row = firstRow;
+ var foldLine = this.session.getNextFoldLine(row);
+ var foldStart = foldLine ? foldLine.start.row : Infinity;
+
+ while (true) {
+ if (row > foldStart) {
+ row = foldLine.end.row+1;
+ foldLine = this.session.getNextFoldLine(row, foldLine);
+ foldStart = foldLine ? foldLine.start.row :Infinity;
+ }
+ if (row > lastRow)
+ break;
+
+ if (this.$useLineGroups())
+ html.push("