mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-02 16:35:19 +02:00
feat(issue search): query string for boolean operators and phrase search (#6952)
closes #6909 related to forgejo/design#14 # Description Adds the following boolean operators for issues when using an indexer (with minor caveats) - `+term`: `term` MUST be present for any result - `-term`: negation; exclude results that contain `term` - `"this is a term"`: matches the exact phrase `this is a term` In all cases the special characters may be escaped by the prefix `\` Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6952 Reviewed-by: 0ko <0ko@noreply.codeberg.org> Reviewed-by: Otto <otto@codeberg.org> Co-authored-by: Shiny Nematoda <snematoda.751k2@aleeas.com> Co-committed-by: Shiny Nematoda <snematoda.751k2@aleeas.com>
This commit is contained in:
parent
eaa641c21e
commit
cddf608cb9
19 changed files with 451 additions and 192 deletions
112
modules/indexer/issues/internal/qstring.go
Normal file
112
modules/indexer/issues/internal/qstring.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type BoolOpt int
|
||||
|
||||
const (
|
||||
BoolOptMust BoolOpt = iota
|
||||
BoolOptShould
|
||||
BoolOptNot
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
Term string
|
||||
Kind BoolOpt
|
||||
Fuzzy bool
|
||||
}
|
||||
|
||||
type Tokenizer struct {
|
||||
in *strings.Reader
|
||||
}
|
||||
|
||||
func (t *Tokenizer) next() (tk Token, err error) {
|
||||
var (
|
||||
sb strings.Builder
|
||||
r rune
|
||||
)
|
||||
tk.Kind = BoolOptShould
|
||||
tk.Fuzzy = true
|
||||
|
||||
// skip all leading white space
|
||||
for {
|
||||
if r, _, err = t.in.ReadRune(); err == nil && r == ' ' {
|
||||
//nolint:staticcheck,wastedassign // SA4006 the variable is used after the loop
|
||||
r, _, err = t.in.ReadRune()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return tk, err
|
||||
}
|
||||
|
||||
// check for +/- op, increment to the next rune in both cases
|
||||
switch r {
|
||||
case '+':
|
||||
tk.Kind = BoolOptMust
|
||||
r, _, err = t.in.ReadRune()
|
||||
case '-':
|
||||
tk.Kind = BoolOptNot
|
||||
r, _, err = t.in.ReadRune()
|
||||
}
|
||||
if err != nil {
|
||||
return tk, err
|
||||
}
|
||||
|
||||
// parse the string, escaping special characters
|
||||
for esc := false; err == nil; r, _, err = t.in.ReadRune() {
|
||||
if esc {
|
||||
if !strings.ContainsRune("+-\\\"", r) {
|
||||
sb.WriteRune('\\')
|
||||
}
|
||||
sb.WriteRune(r)
|
||||
esc = false
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '\\':
|
||||
esc = true
|
||||
case '"':
|
||||
if !tk.Fuzzy {
|
||||
goto nextEnd
|
||||
}
|
||||
tk.Fuzzy = false
|
||||
case ' ', '\t':
|
||||
if tk.Fuzzy {
|
||||
goto nextEnd
|
||||
}
|
||||
sb.WriteRune(r)
|
||||
default:
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
nextEnd:
|
||||
|
||||
tk.Term = sb.String()
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
} // do not consider EOF as an error at the end
|
||||
return tk, err
|
||||
}
|
||||
|
||||
// Tokenize the keyword
|
||||
func (o *SearchOptions) Tokens() (tokens []Token, err error) {
|
||||
in := strings.NewReader(o.Keyword)
|
||||
it := Tokenizer{in: in}
|
||||
|
||||
for token, err := it.next(); err == nil; token, err = it.next() {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue