Adding in snippet search functionality
http://feedback.gitlab.com/forums/176466-general/suggestions/5529795-search-though-snippets
This commit is contained in:
parent
8a7d10af23
commit
4cca1b050a
12 changed files with 278 additions and 34 deletions
|
@ -14,6 +14,12 @@ class SearchController < ApplicationController
|
|||
end
|
||||
|
||||
Search::ProjectService.new(@project, current_user, params).execute
|
||||
elsif params[:snippets].eql? 'true'
|
||||
unless %w(snippet_blobs snippet_titles).include?(@scope)
|
||||
@scope = 'snippet_blobs'
|
||||
end
|
||||
|
||||
Search::SnippetService.new(current_user, params).execute
|
||||
else
|
||||
unless %w(projects issues merge_requests).include?(@scope)
|
||||
@scope = 'projects'
|
||||
|
|
|
@ -178,6 +178,8 @@ module ApplicationHelper
|
|||
def search_placeholder
|
||||
if @project && @project.persisted?
|
||||
"Search in this project"
|
||||
elsif @snippet || @snippets || (params && params[:snippets] == 'true')
|
||||
'Search snippets'
|
||||
elsif @group && @group.persisted?
|
||||
"Search in this group"
|
||||
else
|
||||
|
|
|
@ -65,4 +65,18 @@ class Snippet < ActiveRecord::Base
|
|||
def expired?
|
||||
expires_at && expires_at < Time.current
|
||||
end
|
||||
|
||||
class << self
|
||||
def search(query)
|
||||
where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%")
|
||||
end
|
||||
|
||||
def search_code(query)
|
||||
where('(content LIKE :query)', query: "%#{query}%")
|
||||
end
|
||||
|
||||
def accessible_to(user)
|
||||
where('private = ? OR author_id = ?', false, user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
14
app/services/search/snippet_service.rb
Normal file
14
app/services/search/snippet_service.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
module Search
|
||||
class SnippetService
|
||||
attr_accessor :current_user, :params
|
||||
|
||||
def initialize(user, params)
|
||||
@current_user, @params = user, params.dup
|
||||
end
|
||||
|
||||
def execute
|
||||
snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
|
||||
Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,8 @@
|
|||
- if @project && @project.persisted?
|
||||
= hidden_field_tag :project_id, @project.id
|
||||
= hidden_field_tag :search_code, true
|
||||
- if @snippet || @snippets
|
||||
= hidden_field_tag :snippets, true
|
||||
= hidden_field_tag :repository_ref, @ref
|
||||
= submit_tag 'Go' if ENV['RAILS_ENV'] == 'test'
|
||||
.search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
|
||||
|
|
|
@ -1,35 +1,36 @@
|
|||
.dropdown.inline
|
||||
%a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
|
||||
%i.icon-tags
|
||||
%span.light Group:
|
||||
- if @group.present?
|
||||
%strong= @group.name
|
||||
- else
|
||||
Any
|
||||
%b.caret
|
||||
%ul.dropdown-menu
|
||||
%li
|
||||
= link_to search_filter_path(group_id: nil) do
|
||||
- unless params[:snippets]
|
||||
.dropdown.inline
|
||||
%a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
|
||||
%i.icon-tags
|
||||
%span.light Group:
|
||||
- if @group.present?
|
||||
%strong= @group.name
|
||||
- else
|
||||
Any
|
||||
- current_user.authorized_groups.sort_by(&:name).each do |group|
|
||||
%b.caret
|
||||
%ul.dropdown-menu
|
||||
%li
|
||||
= link_to search_filter_path(group_id: group.id, project_id: nil) do
|
||||
= group.name
|
||||
= link_to search_filter_path(group_id: nil) do
|
||||
Any
|
||||
- current_user.authorized_groups.sort_by(&:name).each do |group|
|
||||
%li
|
||||
= link_to search_filter_path(group_id: group.id, project_id: nil) do
|
||||
= group.name
|
||||
|
||||
.dropdown.inline.prepend-left-10.project-filter
|
||||
%a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
|
||||
%i.icon-tags
|
||||
%span.light Project:
|
||||
- if @project.present?
|
||||
%strong= @project.name_with_namespace
|
||||
- else
|
||||
Any
|
||||
%b.caret
|
||||
%ul.dropdown-menu
|
||||
%li
|
||||
= link_to search_filter_path(project_id: nil) do
|
||||
.dropdown.inline.prepend-left-10.project-filter
|
||||
%a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
|
||||
%i.icon-tags
|
||||
%span.light Project:
|
||||
- if @project.present?
|
||||
%strong= @project.name_with_namespace
|
||||
- else
|
||||
Any
|
||||
- current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
|
||||
%b.caret
|
||||
%ul.dropdown-menu
|
||||
%li
|
||||
= link_to search_filter_path(project_id: project.id, group_id: nil) do
|
||||
= project.name_with_namespace
|
||||
= link_to search_filter_path(project_id: nil) do
|
||||
Any
|
||||
- current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
|
||||
%li
|
||||
= link_to search_filter_path(project_id: project.id, group_id: nil) do
|
||||
= project.name_with_namespace
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
%h4
|
||||
#{@search_results.total_count} results found
|
||||
- if @project
|
||||
for #{link_to @project.name_with_namespace, @project}
|
||||
- elsif @group
|
||||
for #{link_to @group.name, @group}
|
||||
- unless params[:snippets].eql? 'true'
|
||||
- if @project
|
||||
for #{link_to @project.name_with_namespace, @project}
|
||||
- elsif @group
|
||||
for #{link_to @group.name, @group}
|
||||
|
||||
%hr
|
||||
|
||||
|
@ -11,6 +12,8 @@
|
|||
.col-sm-3
|
||||
- if @project
|
||||
= render "project_filter"
|
||||
- elsif params[:snippets].eql? 'true'
|
||||
= render 'snippet_filter'
|
||||
- else
|
||||
= render "global_filter"
|
||||
.col-sm-9
|
||||
|
|
13
app/views/search/_snippet_filter.html.haml
Normal file
13
app/views/search/_snippet_filter.html.haml
Normal file
|
@ -0,0 +1,13 @@
|
|||
%ul.nav.nav-pills.nav-stacked.search-filter
|
||||
%li{class: ("active" if @scope == 'snippet_blobs')}
|
||||
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
|
||||
%i.icon-code
|
||||
Code
|
||||
.pull-right
|
||||
= @search_results.snippet_blobs_count
|
||||
%li{class: ("active" if @scope == 'snippet_titles')}
|
||||
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
|
||||
%i.icon-book
|
||||
Titles and Filenames
|
||||
.pull-right
|
||||
= @search_results.snippet_titles_count
|
65
app/views/search/results/_snippet_blob.html.haml
Normal file
65
app/views/search/results/_snippet_blob.html.haml
Normal file
|
@ -0,0 +1,65 @@
|
|||
.search-result-row
|
||||
%span
|
||||
= snippet_blob[:snippet_object].title
|
||||
by
|
||||
= link_to user_snippets_path(snippet_blob[:snippet_object].author) do
|
||||
= image_tag avatar_icon(snippet_blob[:snippet_object].author_email), class: "avatar avatar-inline s16", alt: ''
|
||||
= snippet_blob[:snippet_object].author_name
|
||||
%span.light #{time_ago_with_tooltip(snippet_blob[:snippet_object].created_at)}
|
||||
%h4.snippet-title
|
||||
- snippet_path = reliable_snippet_path(snippet_blob[:snippet_object])
|
||||
= link_to snippet_path do
|
||||
.file-holder
|
||||
.file-title
|
||||
%i.icon-file
|
||||
%strong= snippet_blob[:snippet_object].file_name
|
||||
%span.options
|
||||
.btn-group.tree-btn-group.pull-right
|
||||
- if snippet_blob[:snippet_object].author == current_user
|
||||
= link_to "Edit", edit_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", title: 'Edit Snippet'
|
||||
= link_to "Delete", snippet_path(snippet_blob[:snippet_object]), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-tiny", title: 'Delete Snippet'
|
||||
= link_to "Raw", raw_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", target: "_blank"
|
||||
- if gitlab_markdown?(snippet_blob[:snippet_object].file_name)
|
||||
.file-content.wiki
|
||||
- snippet_blob[:snippet_chunks].each do |snippet|
|
||||
- unless snippet[:data].empty?
|
||||
= preserve do
|
||||
= markdown(snippet[:data])
|
||||
- else
|
||||
.file-content.code
|
||||
.nothing-here-block Empty file
|
||||
- elsif markup?(snippet_blob[:snippet_object].file_name)
|
||||
.file-content.wiki
|
||||
- snippet_blob[:snippet_chunks].each do |snippet|
|
||||
- unless snippet[:data].empty?
|
||||
= render_markup(snippet_blob[:snippet_object].file_name, snippet[:data])
|
||||
- else
|
||||
.file-content.code
|
||||
.nothing-here-block Empty file
|
||||
- else
|
||||
.file-content.code
|
||||
%div.highlighted-data{class: user_color_scheme_class}
|
||||
.line-numbers
|
||||
- snippet_blob[:snippet_chunks].each do |snippet|
|
||||
- unless snippet[:data].empty?
|
||||
- snippet[:data].lines.to_a.size.times do |index|
|
||||
- offset = defined?(snippet[:start_line]) ? snippet[:start_line] : 1
|
||||
- i = index + offset
|
||||
= link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}" do
|
||||
%i.icon-link
|
||||
= i
|
||||
- unless snippet == snippet_blob[:snippet_chunks].last
|
||||
%a
|
||||
= "."
|
||||
.highlight.term
|
||||
%pre
|
||||
%code
|
||||
- snippet_blob[:snippet_chunks].each do |snippet|
|
||||
- unless snippet[:data].empty?
|
||||
= snippet[:data]
|
||||
- unless snippet == snippet_blob[:snippet_chunks].last
|
||||
%a
|
||||
= "..."
|
||||
- else
|
||||
.file-content.code
|
||||
.nothing-here-block Empty file
|
23
app/views/search/results/_snippet_title.html.haml
Normal file
23
app/views/search/results/_snippet_title.html.haml
Normal file
|
@ -0,0 +1,23 @@
|
|||
.search-result-row
|
||||
%h4.snippet-title.term
|
||||
= link_to reliable_snippet_path(snippet_title) do
|
||||
= truncate(snippet_title.title, length: 60)
|
||||
- if snippet_title.private?
|
||||
%span.label.label-gray
|
||||
%i.icon-lock
|
||||
private
|
||||
%span.cgray.monospace.tiny.pull-right.term
|
||||
= snippet_title.file_name
|
||||
|
||||
%small.pull-right.cgray
|
||||
- if snippet_title.project_id?
|
||||
= link_to snippet_title.project.name_with_namespace, project_path(snippet_title.project)
|
||||
|
||||
.snippet-info
|
||||
= "##{snippet_title.id}"
|
||||
%span
|
||||
by
|
||||
= link_to user_snippets_path(snippet_title.author) do
|
||||
= image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: ''
|
||||
= snippet_title.author_name
|
||||
%span.light #{time_ago_with_tooltip(snippet_title.created_at)}
|
|
@ -13,6 +13,7 @@
|
|||
= render 'filter', f: f
|
||||
= hidden_field_tag :project_id, params[:project_id]
|
||||
= hidden_field_tag :group_id, params[:group_id]
|
||||
= hidden_field_tag :snippets, params[:snippets]
|
||||
= hidden_field_tag :scope, params[:scope]
|
||||
|
||||
.results.prepend-top-10
|
||||
|
|
100
lib/gitlab/snippet_search_results.rb
Normal file
100
lib/gitlab/snippet_search_results.rb
Normal file
|
@ -0,0 +1,100 @@
|
|||
module Gitlab
|
||||
class SnippetSearchResults < SearchResults
|
||||
attr_reader :limit_snippet_ids
|
||||
|
||||
def initialize(limit_snippet_ids, query)
|
||||
@limit_snippet_ids = limit_snippet_ids
|
||||
@query = query
|
||||
end
|
||||
|
||||
def objects(scope, page = nil)
|
||||
case scope
|
||||
when 'snippet_titles'
|
||||
Kaminari.paginate_array(snippet_titles).page(page).per(per_page)
|
||||
when 'snippet_blobs'
|
||||
Kaminari.paginate_array(snippet_blobs).page(page).per(per_page)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def total_count
|
||||
@total_count ||= snippet_titles_count + snippet_blobs_count
|
||||
end
|
||||
|
||||
def snippet_titles_count
|
||||
@snippet_titles_count ||= snippet_titles.count
|
||||
end
|
||||
|
||||
def snippet_blobs_count
|
||||
@snippet_blobs_count ||= snippet_blobs.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def snippet_titles
|
||||
Snippet.where(id: limit_snippet_ids).search(query).order('updated_at DESC')
|
||||
end
|
||||
|
||||
def snippet_blobs
|
||||
matching_snippets = Snippet.where(id: limit_snippet_ids).search_code(query).order('updated_at DESC')
|
||||
matching_snippets = matching_snippets.to_a
|
||||
snippets = []
|
||||
matching_snippets.each { |e| snippets << chunk_snippet(e) }
|
||||
snippets
|
||||
end
|
||||
|
||||
def default_scope
|
||||
'snippet_blobs'
|
||||
end
|
||||
|
||||
def bounded_line_numbers(line, min, max, surrounding_lines)
|
||||
lower = line - surrounding_lines > min ? line - surrounding_lines : min
|
||||
upper = line + surrounding_lines < max ? line + surrounding_lines : max
|
||||
(lower..upper).to_a
|
||||
end
|
||||
|
||||
def chunk_snippet(snippet)
|
||||
surrounding_lines = 3
|
||||
used_lines = []
|
||||
lined_content = snippet.content.split("\n")
|
||||
lined_content.each_with_index { |line, line_number|
|
||||
used_lines.concat bounded_line_numbers(
|
||||
line_number,
|
||||
0,
|
||||
lined_content.size,
|
||||
surrounding_lines
|
||||
) if line.include?(query)
|
||||
}
|
||||
|
||||
used_lines = used_lines.uniq.sort
|
||||
|
||||
snippet_chunk = []
|
||||
snippet_chunks = []
|
||||
snippet_start_line = 0
|
||||
last_line = -1
|
||||
used_lines.each { |line_number|
|
||||
if last_line < 0
|
||||
snippet_start_line = line_number
|
||||
snippet_chunk << lined_content[line_number]
|
||||
elsif last_line == line_number - 1
|
||||
snippet_chunk << lined_content[line_number]
|
||||
else
|
||||
snippet_chunks << {
|
||||
data: snippet_chunk.join("\n"),
|
||||
start_line: snippet_start_line + 1
|
||||
}
|
||||
snippet_chunk = [lined_content[line_number]]
|
||||
snippet_start_line = line_number
|
||||
end
|
||||
last_line = line_number
|
||||
}
|
||||
snippet_chunks << {
|
||||
data: snippet_chunk.join("\n"),
|
||||
start_line: snippet_start_line + 1
|
||||
}
|
||||
|
||||
{ snippet_object: snippet, snippet_chunks: snippet_chunks }
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue