commit 55c3548
sewn
·
2026-07-23 17:28:12 +0000 UTC
parent 7c80055
Revert "Add spell checking via harper-ls" This reverts commit 8f6b08f151bb8fc4b2b301685b9def045cd04f27.
7 files changed,
+2,
-515
M
app.go
+0,
-30
1@@ -154,8 +154,6 @@ type App struct {
2 shownPasteHint bool
3
4 closing atomic.Bool
5-
6- harper *harperState
7 }
8
9 func NewApp(cfg Config) (app *App, err error) {
10@@ -252,7 +250,6 @@ func (app *App) Close() {
11 session.Close()
12 }
13 ui.DBusStop()
14- app.harperClose()
15 app.closing.Store(true)
16 go func() {
17 // drain remaining events
18@@ -275,7 +272,6 @@ func (app *App) Run() {
19 if app.lastCloseTime.IsZero() {
20 app.lastCloseTime = time.Now()
21 }
22- app.harperInit()
23 go app.uiLoop()
24 go app.ircLoop("")
25 app.eventLoop()
26@@ -688,16 +684,6 @@ func (app *App) handleUIEvent(ev interface{}) bool {
27 } else {
28 app.uploadingProgress = &ev.Progress
29 }
30- case *events.EventSpellCheck:
31- text := string(app.win.InputContent())
32- typos := make([]events.TypoRange, len(ev.Typos))
33- for i, t := range ev.Typos {
34- typos[i] = events.TypoRange{
35- Start: utf16OffsetToRuneIndex(text, t.Start),
36- End: utf16OffsetToRuneIndex(text, t.End),
37- }
38- }
39- app.win.SetTypos(typos)
40 case *events.EventOpenLink:
41 host, target, netID := app.findNetworkByLink(ev.Link)
42 if netID != "" {
43@@ -855,13 +841,11 @@ func (app *App) handleMouseEvent(ev vaxis.Mouse) {
44 if i := app.win.VerticalBufferOffset(y); i == app.win.ClickedBuffer() {
45 app.win.GoToBufferNo(i)
46 app.clearBufferCommand()
47- app.spellCheck()
48 }
49 } else if app.win.ChannelWidth() == 0 && y == h-1 {
50 if i := app.win.HorizontalBufferOffset(x); i >= 0 && i == app.win.ClickedBuffer() {
51 app.win.GoToBufferNo(i)
52 app.clearBufferCommand()
53- app.spellCheck()
54 }
55 } else if x > w-app.win.MemberWidth() && y < h-memberItems*2 {
56 if i := y - 2 + app.win.MemberOffset(); i >= 0 && i == app.win.ClickedMember() {
57@@ -932,7 +916,6 @@ func (app *App) handleAction(action string, args ...string) {
58 case "quit":
59 if app.win.InputClear() {
60 app.typing()
61- app.spellCheck()
62 } else {
63 app.win.InputSet("/quit")
64 }
65@@ -953,19 +936,15 @@ func (app *App) handleAction(action string, args ...string) {
66 case "buffer-next":
67 app.win.NextBuffer()
68 app.win.ScrollToBuffer()
69- app.spellCheck()
70 case "buffer-previous":
71 app.win.PreviousBuffer()
72 app.win.ScrollToBuffer()
73- app.spellCheck()
74 case "buffer-next-unread":
75 app.win.NextUnreadBuffer()
76 app.win.ScrollToBuffer()
77- app.spellCheck()
78 case "buffer-previous-unread":
79 app.win.PreviousUnreadBuffer()
80 app.win.ScrollToBuffer()
81- app.spellCheck()
82 case "cursor-right-word":
83 app.win.InputRightWord()
84 case "cursor-left-word":
85@@ -981,39 +960,32 @@ func (app *App) handleAction(action string, args ...string) {
86 case "cursor-delete-previous-word":
87 if app.win.InputDeleteWord() {
88 app.typing()
89- app.spellCheck()
90 }
91 case "cursor-delete-next-word":
92 if app.win.InputDeleteNextWord() {
93 app.typing()
94- app.spellCheck()
95 }
96 case "cursor-delete-previous":
97 if app.win.InputBackspace() {
98 app.typing()
99- app.spellCheck()
100 }
101 case "cursor-delete-next":
102 if app.win.InputDelete() {
103 app.typing()
104- app.spellCheck()
105 }
106 case "cursor-delete-before":
107 if app.win.InputDeleteBefore() {
108 app.typing()
109- app.spellCheck()
110 }
111 case "cursor-delete-after":
112 if app.win.InputDeleteAfter() {
113 app.typing()
114- app.spellCheck()
115 }
116 case "search-editor":
117 app.win.InputBackSearch()
118 case "auto-complete":
119 if app.win.InputAutoComplete() {
120 app.typing()
121- app.spellCheck()
122 }
123 case "close-overlay":
124 app.win.CloseOverlay()
125@@ -1053,7 +1025,6 @@ func (app *App) handleAction(action string, args ...string) {
126 maxInt := int(^uint(0) >> 1)
127 app.win.GoToBufferNo(maxInt)
128 }
129- app.spellCheck()
130 }
131 case "paste-hint":
132 if !app.shownPasteHint {
133@@ -1165,7 +1136,6 @@ func (app *App) handleKeyEvent(ev vaxis.Key) {
134 app.win.InputRune(r)
135 }
136 app.typing()
137- app.spellCheck()
138 return
139 }
140
+2,
-12
1@@ -98,9 +98,8 @@ type Config struct {
2
3 Channels []string
4
5- Typings bool
6- Mouse bool
7- SpellCheck bool
8+ Typings bool
9+ Mouse bool
10
11 Highlights []string
12 OnHighlightPath string
13@@ -145,7 +144,6 @@ func Defaults() Config {
14 Channels: nil,
15 Typings: true,
16 Mouse: true,
17- SpellCheck: false,
18 Highlights: nil,
19 OnHighlightPath: "",
20 OnHighlightBeep: false,
21@@ -387,14 +385,6 @@ func unmarshal(filename string, cfg *Config) (err error) {
22 if cfg.Mouse, err = strconv.ParseBool(mouse); err != nil {
23 return err
24 }
25- case "spell-check":
26- var spellCheck string
27- if err := d.ParseParams(&spellCheck); err != nil {
28- return err
29- }
30- if cfg.SpellCheck, err = strconv.ParseBool(spellCheck); err != nil {
31- return err
32- }
33 case "colors":
34 for _, child := range d.Children {
35 var colorStr string
+0,
-4
1@@ -122,10 +122,6 @@ pane-widths {
2 *mouse*
3 Enable or disable mouse support. Defaults to true.
4
5-*spell-check*
6- Enable spell checking using harper-ls. Requires harper-ls to be installed.
7- English only for now. Defaults to false.
8-
9 *colors* { ... }
10 Settings for colors of different UI elements.
11
+0,
-9
1@@ -49,12 +49,3 @@ type EventFileUpload struct {
2 type EventOpenLink struct {
3 Link string
4 }
5-
6-type TypoRange struct {
7- Start int
8- End int
9-}
10-
11-type EventSpellCheck struct {
12- Typos []TypoRange
13-}
+0,
-439
1@@ -1,439 +0,0 @@
2-package senpai
3-
4-import (
5- "bufio"
6- "encoding/json"
7- "fmt"
8- "io"
9- "os"
10- "os/exec"
11- "slices"
12- "strconv"
13- "strings"
14- "unicode/utf16"
15-
16- "git.sr.ht/~delthas/senpai/events"
17- "git.sr.ht/~delthas/senpai/ui"
18-)
19-
20-// Rules disabled for informal IM context.
21-// Full list: https://writewithharper.com/docs/rules
22-var harperDisabledLinters = map[string]bool{
23- // Formality
24- "SentenceCapitalization": false,
25- "CapitalizePersonalPronouns": false,
26- "LongSentences": false,
27- "UnclosedQuotes": false,
28- "OxfordComma": false,
29- "Spaces": false,
30- "NoFrenchSpaces": false,
31- "AvoidCurses": false,
32- "Hedging": false,
33- "FillerWords": false,
34- "DiscourseMarkers": false,
35-
36- // Initialisms (btw, imo, ttyl, brb, etc.)
37- "ByTheWay": false,
38- "InMyOpinion": false,
39- "InMyHumbleOpinion": false,
40- "TalkToYouLater": false,
41- "ForWhatItsWorth": false,
42- "AsFarAsIKnow": false,
43- "BeRightBack": false,
44- "OhMyGod": false,
45- "AsSoonAsPossible": false,
46- "InRealLife": false,
47- "ForYourInformation": false,
48- "IDontKnow": false,
49- "IfYouKnowYouKnow": false,
50- "InCaseYouMissedIt": false,
51- "ToBeHonest": false,
52- "PleaseTakeALook": false,
53- "NeverMind": false,
54- "IfIRecallCorrectly": false,
55- "Really": false,
56- "ExplainLikeImFive": false,
57- "RedundantIIRC": false,
58-
59- // Casual abbreviations
60- "ExpandThrough": false,
61- "ExpandWith": false,
62- "ExpandWithout": false,
63- "ExpandForward": false,
64- "ExpandBecause": false,
65- "ExpandPrevious": false,
66- "ExpandControl": false,
67- "ExpandMinimum": false,
68- "ExpandTimeShorthands": false,
69- "ExpandMemoryShorthands": false,
70- "ExpandParameter": false,
71- "ExpandDependencies": false,
72- "ExpandStandardInputAndOutput": false,
73- "Cybersec": false,
74-
75- // Overly prescriptive style
76- "Dashes": false,
77- "EllipsisLength": false,
78- "SendAnEmailTo": false,
79- "Touristic": false,
80- "BoringWords": false,
81- "Freezing": false,
82- "Starving": false,
83- "Excellent": false,
84- "FatalOutcome": false,
85- "AvoidAndAlso": false,
86- "CondenseAllThe": false,
87- "DotInitialisms": false,
88-
89- // Too aggressive for technical/casual chat
90- "SplitWords": false,
91- "OrthographicConsistency": false,
92- "ToDoHyphen": false,
93- "PhrasalVerbAsCompoundNoun": false,
94- "DisjointPrefixes": false,
95- "NeedToNoun": false,
96- "MissingTo": false,
97- "MoreAdjective": false,
98- "RightClick": false,
99- "MassNouns": false,
100- "HaveTakeALook": false,
101-}
102-
103-type harperConfig struct {
104- HarperLS harperLSConfig `json:"harper-ls"`
105-}
106-
107-type harperLSConfig struct {
108- Linters map[string]bool `json:"linters"`
109- UserDictPath string `json:"userDictPath,omitempty"`
110-}
111-
112-type lspPosition struct {
113- Character int `json:"character"`
114-}
115-
116-type lspRange struct {
117- Start lspPosition `json:"start"`
118- End lspPosition `json:"end"`
119-}
120-
121-type lspDiagnostic struct {
122- Range lspRange `json:"range"`
123-}
124-
125-type lspMessage struct {
126- ID json.RawMessage `json:"id"`
127- Method string `json:"method"`
128- Params json.RawMessage `json:"params"`
129-}
130-
131-type lspDiagnosticsParams struct {
132- Diagnostics []lspDiagnostic `json:"diagnostics"`
133-}
134-
135-type harperState struct {
136- cmd *exec.Cmd
137- stdin io.WriteCloser
138- stdout *bufio.Reader
139- writes chan []byte
140- textCh chan string
141-
142- reqID int
143- ver int
144- dictPath string
145- dictPrev string
146-}
147-
148-func (app *App) harperInit() {
149- if !app.cfg.SpellCheck || app.cfg.Transient {
150- return
151- }
152- path, err := exec.LookPath("harper-ls")
153- if err != nil {
154- return
155- }
156- cmd := exec.Command(path, "--stdio")
157- stdin, err := cmd.StdinPipe()
158- if err != nil {
159- return
160- }
161- stdout, err := cmd.StdoutPipe()
162- if err != nil {
163- stdin.Close()
164- return
165- }
166- if err := cmd.Start(); err != nil {
167- stdin.Close()
168- return
169- }
170-
171- dictFile, err := os.CreateTemp("", "senpai-harper-dict-*.txt")
172- if err != nil {
173- stdin.Close()
174- cmd.Process.Kill()
175- return
176- }
177- dictFile.Close()
178-
179- h := &harperState{
180- cmd: cmd,
181- stdin: stdin,
182- stdout: bufio.NewReader(stdout),
183- writes: make(chan []byte, 64),
184- textCh: make(chan string, 1),
185- dictPath: dictFile.Name(),
186- }
187- go h.writeLoop()
188-
189- if err := h.sendRequest("initialize", json.RawMessage(`{
190- "capabilities": {
191- "textDocument": {
192- "publishDiagnostics": {}
193- }
194- },
195- "rootUri": null,
196- "processId": null
197- }`)); err != nil {
198- cmd.Process.Kill()
199- return
200- }
201-
202- if err := h.sendNotification("initialized", json.RawMessage(`{}`)); err != nil {
203- cmd.Process.Kill()
204- return
205- }
206-
207- app.harper = h
208- go app.harperLoop()
209-}
210-
211-func (app *App) harperLoop() {
212- h := app.harper
213- opened := false
214- for {
215- msg, err := h.readMessage()
216- if err != nil {
217- app.queueStatusLine("", ui.Line{
218- Head: ui.PlainString("--"),
219- Body: ui.PlainString("spell checker error: " + err.Error()),
220- })
221- return
222- }
223- var envelope lspMessage
224- if err := json.Unmarshal(msg, &envelope); err != nil {
225- continue
226- }
227- if envelope.ID != nil && envelope.Method != "" {
228- switch envelope.Method {
229- case "workspace/configuration":
230- cfg, _ := json.Marshal([]harperConfig{{HarperLS: harperLSConfig{Linters: harperDisabledLinters, UserDictPath: h.dictPath}}})
231- h.sendResponse(envelope.ID, cfg)
232- if !opened {
233- opened = true
234- h.sendNotification("textDocument/didOpen", json.RawMessage(`{
235- "textDocument": {
236- "uri": "file:///senpai-input.txt",
237- "languageId": "plaintext",
238- "version": 0,
239- "text": ""
240- }
241- }`))
242- }
243- default:
244- h.sendResponse(envelope.ID, json.RawMessage(`null`))
245- }
246- continue
247- }
248- if envelope.Method != "textDocument/publishDiagnostics" {
249- continue
250- }
251- var params lspDiagnosticsParams
252- if err := json.Unmarshal(envelope.Params, ¶ms); err != nil {
253- continue
254- }
255- var typos []events.TypoRange
256- for _, d := range params.Diagnostics {
257- typos = append(typos, events.TypoRange{
258- Start: d.Range.Start.Character,
259- End: d.Range.End.Character,
260- })
261- }
262- app.postEvent(event{
263- src: "*",
264- content: &events.EventSpellCheck{Typos: typos},
265- })
266- }
267-}
268-
269-func (app *App) spellCheck() {
270- if app.harper == nil {
271- return
272- }
273- input := app.win.InputContent()
274- if len(input) == 0 {
275- app.win.SetTypos(nil)
276- return
277- }
278- if isCommand(input) {
279- app.win.SetTypos(nil)
280- return
281- }
282- app.updateHarperDict()
283- app.harper.sendChange(string(input))
284-}
285-
286-func (app *App) updateHarperDict() {
287- netID, _ := app.win.CurrentBuffer()
288- s := app.sessions[netID]
289- if s == nil {
290- return
291- }
292- words := s.Users()
293- slices.Sort(words)
294- content := strings.Join(words, "\n")
295- if content == app.harper.dictPrev {
296- return
297- }
298- app.harper.dictPrev = content
299- os.WriteFile(app.harper.dictPath, []byte(content), 0600)
300-}
301-
302-func (app *App) harperClose() {
303- if app.harper == nil {
304- return
305- }
306- os.Remove(app.harper.dictPath)
307- app.harper.sendNotification("shutdown", json.RawMessage(`null`))
308- close(app.harper.writes)
309- app.harper.cmd.Process.Kill()
310-}
311-
312-func (h *harperState) sendChange(text string) {
313- select {
314- case <-h.textCh:
315- default:
316- }
317- h.textCh <- text
318-}
319-
320-func (h *harperState) sendRequest(method string, params json.RawMessage) error {
321- h.reqID++
322- msg, _ := json.Marshal(map[string]interface{}{
323- "jsonrpc": "2.0",
324- "id": h.reqID,
325- "method": method,
326- "params": params,
327- })
328- return h.writeMessage(msg)
329-}
330-
331-func (h *harperState) sendResponse(id json.RawMessage, result json.RawMessage) error {
332- msg, _ := json.Marshal(map[string]interface{}{
333- "jsonrpc": "2.0",
334- "id": id,
335- "result": result,
336- })
337- return h.writeMessage(msg)
338-}
339-
340-func (h *harperState) sendNotification(method string, params json.RawMessage) error {
341- msg, _ := json.Marshal(map[string]interface{}{
342- "jsonrpc": "2.0",
343- "method": method,
344- "params": params,
345- })
346- return h.writeMessage(msg)
347-}
348-
349-func (h *harperState) writeMessage(msg []byte) error {
350- header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(msg))
351- h.writes <- append([]byte(header), msg...)
352- return nil
353-}
354-
355-func (h *harperState) writeLoop() {
356- for {
357- select {
358- case msg, ok := <-h.writes:
359- if !ok {
360- return
361- }
362-
363- if _, err := h.stdin.Write(msg); err != nil {
364- return
365- }
366- case text := <-h.textCh:
367- h.ver++
368- params, _ := json.Marshal(map[string]interface{}{
369- "textDocument": map[string]interface{}{
370- "uri": "file:///senpai-input.txt",
371- "version": h.ver,
372- },
373- "contentChanges": []map[string]interface{}{
374- {"text": text},
375- },
376- })
377- msg, _ := json.Marshal(map[string]interface{}{
378- "jsonrpc": "2.0",
379- "method": "textDocument/didChange",
380- "params": json.RawMessage(params),
381- })
382-
383- header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(msg))
384- if _, err := h.stdin.Write(append([]byte(header), msg...)); err != nil {
385- return
386- }
387- // Wait for the config response before accepting more text,
388- // so we throttle to harper's processing speed.
389- resp, ok := <-h.writes
390- if !ok {
391- return
392- }
393-
394- if _, err := h.stdin.Write(resp); err != nil {
395- return
396- }
397- }
398- }
399-}
400-
401-func (h *harperState) readMessage() ([]byte, error) {
402- var contentLength int
403- for {
404- line, err := h.stdout.ReadString('\n')
405- if err != nil {
406- return nil, err
407- }
408- line = line[:len(line)-1]
409- if len(line) > 0 && line[len(line)-1] == '\r' {
410- line = line[:len(line)-1]
411- }
412- if line == "" {
413- break
414- }
415- if len(line) > 16 && line[:16] == "Content-Length: " {
416- contentLength, _ = strconv.Atoi(line[16:])
417- }
418- }
419- if contentLength == 0 {
420- return nil, fmt.Errorf("missing content length")
421- }
422- body := make([]byte, contentLength)
423- if _, err := io.ReadFull(h.stdout, body); err != nil {
424- return nil, err
425- }
426- return body, nil
427-}
428-
429-func utf16OffsetToRuneIndex(s string, utf16Offset int) int {
430- runeIdx := 0
431- utf16Pos := 0
432- for _, r := range s {
433- if utf16Pos >= utf16Offset {
434- return runeIdx
435- }
436- utf16Pos += len(utf16.Encode([]rune{r}))
437- runeIdx++
438- }
439- return runeIdx
440-}
+0,
-17
1@@ -3,7 +3,6 @@ package ui
2 import (
3 "strings"
4
5- "git.sr.ht/~delthas/senpai/events"
6 "git.sr.ht/~rockorager/vaxis"
7 )
8
9@@ -74,8 +73,6 @@ type Editor struct {
10 // oldest (lowest) index in text of lines that were changed.
11 // used as an optimization to reduce copying when flushing lines.
12 oldestTextChange int
13-
14- typos []events.TypoRange
15 }
16
17 // NewEditor returns a new Editor.
18@@ -326,7 +323,6 @@ func (e *Editor) Flush() string {
19 e.cursorIdx = 0
20 e.offsetIdx = 0
21 e.autoCache = nil
22- e.typos = nil
23 e.backsearchEnd()
24 e.oldestTextChange = len(e.text) - 1
25 return content
26@@ -586,10 +582,6 @@ func (e *Editor) bumpOldestChange() {
27 }
28 }
29
30-func (e *Editor) SetTypos(typos []events.TypoRange) {
31- e.typos = typos
32-}
33-
34 func (e *Editor) Draw(vx *Vaxis, x0, y int, hint string) {
35 var st vaxis.Style
36
37@@ -623,15 +615,6 @@ func (e *Editor) Draw(vx *Vaxis, x0, y int, hint string) {
38 if i >= autoStart && i < autoEnd {
39 s.UnderlineStyle = vaxis.UnderlineSingle
40 }
41- if s.UnderlineStyle == vaxis.UnderlineOff {
42- for _, t := range e.typos {
43- if i >= t.Start && i < t.End {
44- s.UnderlineStyle = vaxis.UnderlineCurly
45- s.UnderlineColor = e.ui.config.Colors.Gray
46- break
47- }
48- }
49- }
50 if i == autoStart {
51 autoX = x
52 }
M
ui/ui.go
+0,
-4
1@@ -627,10 +627,6 @@ func (ui *UI) InputRune(r rune) {
2 ui.e.PutRune(r)
3 }
4
5-func (ui *UI) SetTypos(typos []events.TypoRange) {
6- ui.e.SetTypos(typos)
7-}
8-
9 // InputEnter returns true if the event was eaten
10 func (ui *UI) InputEnter() bool {
11 return ui.e.Enter()