After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.
This commit is contained in:
1 parent
c018f5fd29
commit
0e50de4c76
1047 files changed
+58364
-63747
No files matched your search
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package spellcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Operation int
|
||||
|
||||
const (
|
||||
Inspect Operation = iota
|
||||
Automatic
|
||||
Add
|
||||
Remove
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Operation Operation
|
||||
Word string
|
||||
Generation, Revision uint64
|
||||
}
|
||||
type Result struct {
|
||||
Request
|
||||
Language string
|
||||
Word string
|
||||
Correct, Available bool
|
||||
Suggestions, Exceptions []string
|
||||
Err, DictionaryError error
|
||||
}
|
||||
type Options struct {
|
||||
Language, ExceptionsPath string
|
||||
OpenDictionary func(string) (Dictionary, error)
|
||||
}
|
||||
type Service struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
requests, automatic chan Request
|
||||
results chan Result
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func New_service(parent context.Context, options Options) *Service {
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
s := &Service{ctx: ctx, cancel: cancel, requests: make(chan Request, 16), automatic: make(chan Request, 32), results: make(chan Result, 32), done: make(chan struct{})}
|
||||
go s.run(options)
|
||||
return s
|
||||
}
|
||||
func (s *Service) Results() <-chan Result { return s.results }
|
||||
func (s *Service) Submit(request Request) bool {
|
||||
queue := s.requests
|
||||
if request.Operation == Automatic {
|
||||
queue = s.automatic
|
||||
}
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return false
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case queue <- request:
|
||||
return true
|
||||
case <-s.ctx.Done():
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func (s *Service) Close() { s.once.Do(s.cancel); <-s.done }
|
||||
|
||||
type commandEntry struct {
|
||||
valid bool
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
// Commands are tested as executable PATH entries, never executed. Builtins are
|
||||
// accepted separately because they have no executable file. Aliases/functions
|
||||
// belong to the child shell and cannot be discovered from the reader process.
|
||||
func valid_command(word string, cache map[string]commandEntry) bool {
|
||||
if strings.ContainsAny(word, "/\\\x00") {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(" alias bg bind break builtin caller cd command compgen complete compopt continue declare dirs disown echo enable eval exec exit export false fc fg getopts hash help history jobs kill let local logout mapfile popd printf pushd pwd read readarray readonly return set shift shopt source test times trap true type typeset ulimit umask unalias unset wait ", " "+word+" ") {
|
||||
return true
|
||||
}
|
||||
if entry, ok := cache[word]; ok && time.Now().Before(entry.expires) {
|
||||
return entry.valid
|
||||
}
|
||||
_, err := exec.LookPath(word)
|
||||
valid := err == nil || errors.Is(err, exec.ErrDot)
|
||||
if len(cache) >= 1024 {
|
||||
clear(cache)
|
||||
}
|
||||
cache[word] = commandEntry{valid, time.Now().Add(30 * time.Second)}
|
||||
return valid
|
||||
}
|
||||
|
||||
func (s *Service) run(options Options) {
|
||||
defer close(s.done)
|
||||
open := options.OpenDictionary
|
||||
if open == nil {
|
||||
open = open_dictionary
|
||||
}
|
||||
dict, dictionaryErr := open(options.Language)
|
||||
if dict != nil {
|
||||
defer dict.Close()
|
||||
}
|
||||
words, storeErr := load_exceptions(options.ExceptionsPath)
|
||||
cache := make(map[string]commandEntry)
|
||||
for {
|
||||
var request Request
|
||||
// Prefer interactive actions over a backlog of automatic checks.
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case request = <-s.requests:
|
||||
default:
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case request = <-s.requests:
|
||||
case request = <-s.automatic:
|
||||
}
|
||||
}
|
||||
result := Result{Request: request, Language: options.Language, Word: Word(request.Word), Available: dict != nil && dictionaryErr == nil, DictionaryError: dictionaryErr, Err: storeErr}
|
||||
if request.Operation == Add || request.Operation == Remove {
|
||||
result.Word = strings.TrimSpace(request.Word)
|
||||
if result.Err == nil && !valid_word(result.Word) {
|
||||
result.Err = fmt.Errorf("enter a single word of at most 128 characters")
|
||||
}
|
||||
if result.Err == nil {
|
||||
var draft map[string]string
|
||||
draft, result.Err = update_exceptions(s.ctx, options.ExceptionsPath, request.Operation, result.Word)
|
||||
if result.Err == nil {
|
||||
words = draft
|
||||
}
|
||||
}
|
||||
} else if result.Err == nil && result.Word != "" {
|
||||
_, result.Correct = words[strings.ToLower(result.Word)]
|
||||
if request.Operation == Automatic && !result.Correct {
|
||||
result.Correct = valid_command(result.Word, cache)
|
||||
}
|
||||
if !result.Correct && result.Available {
|
||||
result.Correct, result.Err = dict.Check(result.Word)
|
||||
}
|
||||
if result.Err == nil && !result.Correct && result.Available && request.Operation == Inspect {
|
||||
result.Suggestions, result.Err = dict.Suggest(result.Word)
|
||||
}
|
||||
}
|
||||
if request.Operation != Automatic {
|
||||
result.Exceptions = exception_list(words)
|
||||
}
|
||||
select {
|
||||
case s.results <- result:
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user