diff options
author | Eric Eastwood <contact@ericeastwood.com> | 2017-05-31 00:50:53 -0500 |
---|---|---|
committer | Eric Eastwood <contact@ericeastwood.com> | 2017-06-15 09:01:56 -0500 |
commit | ea090291bba6bb665b3631cc5a2659e6673a6959 (patch) | |
tree | 1daf4c15aee8afc0eebef94a345eb077d0390632 /lib/gitlab/quick_actions | |
parent | 42aaae9916b7b76da968579fcc722067947df018 (diff) | |
download | gitlab-ce-ea090291bba6bb665b3631cc5a2659e6673a6959.tar.gz |
Rename "Slash commands" to "Quick actions"
Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/27070
Deprecate "chat commands" in favor of "slash commands"
We looked for things like:
- `slash commmand`
- `slash_command`
- `slash-command`
- `SlashCommand`
Diffstat (limited to 'lib/gitlab/quick_actions')
-rw-r--r-- | lib/gitlab/quick_actions/command_definition.rb | 89 | ||||
-rw-r--r-- | lib/gitlab/quick_actions/dsl.rb | 140 | ||||
-rw-r--r-- | lib/gitlab/quick_actions/extractor.rb | 122 |
3 files changed, 351 insertions, 0 deletions
diff --git a/lib/gitlab/quick_actions/command_definition.rb b/lib/gitlab/quick_actions/command_definition.rb new file mode 100644 index 00000000000..3937d9c153a --- /dev/null +++ b/lib/gitlab/quick_actions/command_definition.rb @@ -0,0 +1,89 @@ +module Gitlab + module QuickActions + class CommandDefinition + attr_accessor :name, :aliases, :description, :explanation, :params, + :condition_block, :parse_params_block, :action_block + + def initialize(name, attributes = {}) + @name = name + + @aliases = attributes[:aliases] || [] + @description = attributes[:description] || '' + @explanation = attributes[:explanation] || '' + @params = attributes[:params] || [] + @condition_block = attributes[:condition_block] + @parse_params_block = attributes[:parse_params_block] + @action_block = attributes[:action_block] + end + + def all_names + [name, *aliases] + end + + def noop? + action_block.nil? + end + + def available?(opts) + return true unless condition_block + + context = OpenStruct.new(opts) + context.instance_exec(&condition_block) + end + + def explain(context, opts, arg) + return unless available?(opts) + + if explanation.respond_to?(:call) + execute_block(explanation, context, arg) + else + explanation + end + end + + def execute(context, opts, arg) + return if noop? || !available?(opts) + + execute_block(action_block, context, arg) + end + + def to_h(opts) + context = OpenStruct.new(opts) + + desc = description + if desc.respond_to?(:call) + desc = context.instance_exec(&desc) rescue '' + end + + prms = params + if prms.respond_to?(:call) + prms = Array(context.instance_exec(&prms)) rescue params + end + + { + name: name, + aliases: aliases, + description: desc, + params: prms + } + end + + private + + def execute_block(block, context, arg) + if arg.present? + parsed = parse_params(arg, context) + context.instance_exec(parsed, &block) + elsif block.arity == 0 + context.instance_exec(&block) + end + end + + def parse_params(arg, context) + return arg unless parse_params_block + + context.instance_exec(arg, &parse_params_block) + end + end + end +end diff --git a/lib/gitlab/quick_actions/dsl.rb b/lib/gitlab/quick_actions/dsl.rb new file mode 100644 index 00000000000..a4a97236ffc --- /dev/null +++ b/lib/gitlab/quick_actions/dsl.rb @@ -0,0 +1,140 @@ +module Gitlab + module QuickActions + module Dsl + extend ActiveSupport::Concern + + included do + cattr_accessor :command_definitions, instance_accessor: false do + [] + end + + cattr_accessor :command_definitions_by_name, instance_accessor: false do + {} + end + end + + class_methods do + # Allows to give a description to the next quick action. + # This description is shown in the autocomplete menu. + # It accepts a block that will be evaluated with the context given to + # `CommandDefintion#to_h`. + # + # Example: + # + # desc do + # "This is a dynamic description for #{noteable.to_ability_name}" + # end + # command :command_key do |arguments| + # # Awesome code block + # end + def desc(text = '', &block) + @description = block_given? ? block : text + end + + # Allows to define params for the next quick action. + # These params are shown in the autocomplete menu. + # + # Example: + # + # params "~label ~label2" + # command :command_key do |arguments| + # # Awesome code block + # end + def params(*params, &block) + @params = block_given? ? block : params + end + + # Allows to give an explanation of what the command will do when + # executed. This explanation is shown when rendering the Markdown + # preview. + # + # Example: + # + # explanation do |arguments| + # "Adds label(s) #{arguments.join(' ')}" + # end + # command :command_key do |arguments| + # # Awesome code block + # end + def explanation(text = '', &block) + @explanation = block_given? ? block : text + end + + # Allows to define conditions that must be met in order for the command + # to be returned by `.command_names` & `.command_definitions`. + # It accepts a block that will be evaluated with the context given to + # `CommandDefintion#to_h`. + # + # Example: + # + # condition do + # project.public? + # end + # command :command_key do |arguments| + # # Awesome code block + # end + def condition(&block) + @condition_block = block + end + + # Allows to perform initial parsing of parameters. The result is passed + # both to `command` and `explanation` blocks, instead of the raw + # parameters. + # It accepts a block that will be evaluated with the context given to + # `CommandDefintion#to_h`. + # + # Example: + # + # parse_params do |raw| + # raw.strip + # end + # command :command_key do |parsed| + # # Awesome code block + # end + def parse_params(&block) + @parse_params_block = block + end + + # Registers a new command which is recognizeable from body of email or + # comment. + # It accepts aliases and takes a block. + # + # Example: + # + # command :my_command, :alias_for_my_command do |arguments| + # # Awesome code block + # end + def command(*command_names, &block) + name, *aliases = command_names + + definition = CommandDefinition.new( + name, + aliases: aliases, + description: @description, + explanation: @explanation, + params: @params, + condition_block: @condition_block, + parse_params_block: @parse_params_block, + action_block: block + ) + + self.command_definitions << definition + + definition.all_names.each do |name| + self.command_definitions_by_name[name] = definition + end + + @description = nil + @explanation = nil + @params = nil + @condition_block = nil + @parse_params_block = nil + end + + def definition_by_name(name) + command_definitions_by_name[name.to_sym] + end + end + end + end +end diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb new file mode 100644 index 00000000000..09576be7156 --- /dev/null +++ b/lib/gitlab/quick_actions/extractor.rb @@ -0,0 +1,122 @@ +module Gitlab + module QuickActions + # This class takes an array of commands that should be extracted from a + # given text. + # + # ``` + # extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels]) + # ``` + class Extractor + attr_reader :command_definitions + + def initialize(command_definitions) + @command_definitions = command_definitions + end + + # Extracts commands from content and return an array of commands. + # The array looks like the following: + # [ + # ['command1'], + # ['command3', 'arg1 arg2'], + # ] + # The command and the arguments are stripped. + # The original command text is removed from the given `content`. + # + # Usage: + # ``` + # extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels]) + # msg = %(hello\n/labels ~foo ~"bar baz"\nworld) + # commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']] + # msg #=> "hello\nworld" + # ``` + def extract_commands(content, opts = {}) + return [content, []] unless content + + content = content.dup + + commands = [] + + content.delete!("\r") + content.gsub!(commands_regex(opts)) do + if $~[:cmd] + commands << [$~[:cmd], $~[:arg]].reject(&:blank?) + '' + else + $~[0] + end + end + + [content.strip, commands] + end + + private + + # Builds a regular expression to match known commands. + # First match group captures the command name and + # second match group captures its arguments. + # + # It looks something like: + # + # /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/ + def commands_regex(opts) + names = command_names(opts).map(&:to_s) + + @commands_regex ||= %r{ + (?<code> + # Code blocks: + # ``` + # Anything, including `/cmd arg` which are ignored by this filter + # ``` + + ^``` + .+? + \n```$ + ) + | + (?<html> + # HTML block: + # <tag> + # Anything, including `/cmd arg` which are ignored by this filter + # </tag> + + ^<[^>]+?>\n + .+? + \n<\/[^>]+?>$ + ) + | + (?<html> + # Quote block: + # >>> + # Anything, including `/cmd arg` which are ignored by this filter + # >>> + + ^>>> + .+? + \n>>>$ + ) + | + (?: + # Command not in a blockquote, blockcode, or HTML tag: + # /close + + ^\/ + (?<cmd>#{Regexp.union(names)}) + (?: + [ ] + (?<arg>[^\n]*) + )? + (?:\n|$) + ) + }mx + end + + def command_names(opts) + command_definitions.flat_map do |command| + next if command.noop? + + command.all_names + end.compact + end + end + end +end |