diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..d7202f0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app.go b/app.go index fefa3b8..274a987 100644 --- a/app.go +++ b/app.go @@ -2,29 +2,56 @@ package main import ( "context" + "fmt" "log" + "sort" + "strings" "github.com/wailsapp/wails/v2/pkg/runtime" "lmika.dev/pkg/modash/moslice" + "ucl.lmika.dev/ucl" + "ucl.lmika.dev/ucl/builtins" ) // App struct type App struct { + uclInst *ucl.Inst + ctx context.Context } // NewApp creates a new App application struct func NewApp() *App { - return &App{} + uclInst := ucl.New( + ucl.WithModule(builtins.CSV(nil)), + ucl.WithModule(builtins.Fns()), + ucl.WithModule(builtins.FS(nil)), + ucl.WithModule(builtins.Itrs()), + ucl.WithModule(builtins.Lists()), + ucl.WithModule(builtins.Log(nil)), + ucl.WithModule(builtins.OS()), + ucl.WithModule(builtins.Strs()), + ucl.WithModule(builtins.Time()), + ) + return &App{ + uclInst: uclInst, + } } // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) startup(ctx context.Context) { - a.ctx = ctx + a.ctx = context.WithValue(ctx, uclInstKey, a.uclInst) +} + +func (a *App) ListProcessors() (resp []ListProcessorsResponse) { + for k, v := range TextFilters { + resp = append(resp, ListProcessorsResponse{Name: k, Label: v.Label}) + } + sort.Slice(resp, func(i, j int) bool { return resp[i].Label < resp[j].Label }) + return resp } -// Greet returns a greeting for the given name func (a *App) ProcessText(req ProcessTextRequest) { filter, ok := TextFilters[req.Action] if !ok { @@ -32,23 +59,53 @@ func (a *App) ProcessText(req ProcessTextRequest) { return } - resp, err := moslice.MapWithError(req.Input, func(span TextSpan) (TextSpan, error) { - outStr, err := filter(span.Text) - if err != nil { - return TextSpan{}, err + switch { + case filter.Analyze != nil: + inBfr := strings.Builder{} + for _, span := range req.Input { + if inBfr.Len() > 0 { + inBfr.WriteString("\n") + } + inBfr.WriteString(span.Text) } - return TextSpan{ - Text: outStr, - Pos: span.Pos, - Len: span.Len, - }, nil - }) - if err != nil { - log.Printf("Error running filter: %s", err) - return - } - runtime.EventsEmit(a.ctx, "process-text-response", ProcessTextResponse{ - Output: resp, - }) + msg, err := filter.Analyze(a.ctx, inBfr.String()) + if err != nil { + runtime.EventsEmit(a.ctx, "set-statusbar-message", SetStatusbarMessage{ + Message: fmt.Sprintf("Error running analysis: %v", err.Error()), + Error: true, + }) + return + } + + runtime.EventsEmit(a.ctx, "set-statusbar-message", SetStatusbarMessage{ + Message: msg, + }) + case filter.Filter != nil: + resp, err := moslice.MapWithError(req.Input, func(span TextSpan) (TextSpan, error) { + outRes, err := filter.Filter(a.ctx, span.Text) + if err != nil { + return TextSpan{}, err + } + + return TextSpan{ + Text: outRes.Output, + Pos: span.Pos, + Len: span.Len, + Append: outRes.Append, + }, nil + }) + if err != nil { + runtime.EventsEmit(a.ctx, "set-statusbar-message", SetStatusbarMessage{ + Message: fmt.Sprintf("Error running filter: %v", err.Error()), + Error: true, + }) + return + + } + + runtime.EventsEmit(a.ctx, "process-text-response", ProcessTextResponse{ + Output: resp, + }) + } } diff --git a/appctx.go b/appctx.go new file mode 100644 index 0000000..9701f3f --- /dev/null +++ b/appctx.go @@ -0,0 +1,15 @@ +package main + +import ( + "context" + + "ucl.lmika.dev/ucl" +) + +type uclInstKeyType struct{} + +var uclInstKey = uclInstKeyType{} + +func uclInstFromContext(ctx context.Context) *ucl.Inst { + return ctx.Value(uclInstKey).(*ucl.Inst) +} diff --git a/frontend/index.html b/frontend/index.html index 1ee5df5..4951b76 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,21 +6,18 @@ dequoter -
+
+
+
Cmd+P: open command palette. Shift+Cmd+P: rerun last command.
+
+ data-action="dq-showcommands@window->commands#showCommands dq-rerunlastcommand@window->commands#rerunLastCommand">
+ data-action="keyup.enter->commands#runCommand keyup.down->commands#focusSelect keydown.esc->commands#dismissDialog keyup->commands#handleKeyup">
- +
diff --git a/frontend/src/app.css b/frontend/src/app.css index 2cfbffe..c18533a 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,3 +1,26 @@ +#app { + display: flex; + flex-direction: column; +} + +.editor-mountpoint { + flex-grow: 1; + flex-shrink: 1; +} + +.status-bar { + height: 2em; + background-color: rgb(245, 245, 245); + border-top: solid thin rgb(200, 200, 200); + + align-content: center; + padding-inline: 8px; +} + +.status-bar.deemphasized { + opacity: 0.5; +} + .cm-editor { height: 100%; width: 100%; diff --git a/frontend/src/cmplugins.js b/frontend/src/cmplugins.js index e3c4440..7ee146f 100644 --- a/frontend/src/cmplugins.js +++ b/frontend/src/cmplugins.js @@ -37,4 +37,12 @@ export const commandPalette = keymap.of([{ return true; } -}]); \ No newline at end of file +}, { + key: "Cmd-P", + run: () => { + let event = new CustomEvent('dq-rerunlastcommand'); + window.dispatchEvent(event); + + return true; + } +}]); diff --git a/frontend/src/controllers/commands_controller.js b/frontend/src/controllers/commands_controller.js index 5a04574..622caec 100644 --- a/frontend/src/controllers/commands_controller.js +++ b/frontend/src/controllers/commands_controller.js @@ -1,3 +1,5 @@ +import {ListProcessors} from "../../wailsjs/go/main/App"; + import { Controller } from "@hotwired/stimulus" import { textProcessor } from "../services.js"; @@ -8,7 +10,16 @@ export class CommandsController extends Controller { "commandSelect", ]; - connect() { + async connect() { + this._lastCommand = null; + + let processors = await ListProcessors(); + processors.forEach((processor) => { + let option = document.createElement("option"); + option.value = processor.name; + option.text = processor.label; + this.commandSelectTarget.appendChild(option); + }); this._options = Array.from(this.commandSelectTarget.options); } @@ -29,13 +40,29 @@ export class CommandsController extends Controller { this.element.close(); } + focusSelect(ev) { + this.commandSelectTarget.focus(); + } + runCommand(ev) { ev.preventDefault(); textProcessor.runTextCommand(this.commandSelectTarget.value); + this._lastCommand = this.commandSelectTarget.value; + this.element.close(); } + rerunLastCommand(ev) { + ev.preventDefault(); + + if (this._lastCommand === null) { + return; + } + + textProcessor.runTextCommand(this._lastCommand); + } + _filterOptions(filterText) { let inputText = filterText.toLowerCase(); diff --git a/frontend/src/controllers/status_controller.js b/frontend/src/controllers/status_controller.js new file mode 100644 index 0000000..99504f5 --- /dev/null +++ b/frontend/src/controllers/status_controller.js @@ -0,0 +1,14 @@ +import { Controller } from "@hotwired/stimulus" + +export class StatusController extends Controller { + connect() { + window.runtime.EventsOn("set-statusbar-message", (data) => { + this._setStatusbarMessage(data.message); + }); + } + + _setStatusbarMessage(message) { + this.element.textContent = message; + this.element.classList.remove("deemphasized"); + } +} \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js index d313ede..063030a 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -7,11 +7,13 @@ import {Application} from "@hotwired/stimulus"; import {textProcessor} from "./services.js"; import {multiCursorKeymap, commandPalette} from "./cmplugins.js"; +import {StatusController} from "./controllers/status_controller.js"; import {CommandsController} from "./controllers/commands_controller.js"; + const view = new EditorView({ - parent: document.querySelector("#app"), + parent: document.querySelector("#app .editor-mountpoint"), doc: "", extensions: [ basicSetup, @@ -23,6 +25,7 @@ const view = new EditorView({ window.Stimulus = Application.start() Stimulus.register("commands", CommandsController); +Stimulus.register("status", StatusController); textProcessor.setCodeMirrorEditor(view); diff --git a/frontend/src/services.js b/frontend/src/services.js index c968a4e..7a149f5 100644 --- a/frontend/src/services.js +++ b/frontend/src/services.js @@ -4,11 +4,21 @@ class TextProcessor { setCodeMirrorEditor(editor) { this._editor = editor; window.runtime.EventsOn("process-text-response", (data) => { - const changes = data.output.map(span => ({ - from: span.pos, - to: span.pos + span.len, - insert: span.text, - })); + const changes = data.output.map(span => { + if (span.append) { + return { + from: span.pos + span.len, + to: span.pos + span.len, + insert: span.text, + }; + } else { + return { + from: span.pos, + to: span.pos + span.len, + insert: span.text, + }; + } + }); this._editor.dispatch({ changes: changes }); }); } diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index a164699..a381b11 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -2,4 +2,6 @@ // This file is automatically generated. DO NOT EDIT import {main} from '../models'; +export function ListProcessors():Promise>; + export function ProcessText(arg1:main.ProcessTextRequest):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index b8eff0c..f39aa5b 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -2,6 +2,10 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +export function ListProcessors() { + return window['go']['main']['App']['ListProcessors'](); +} + export function ProcessText(arg1) { return window['go']['main']['App']['ProcessText'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 0315b64..a1f3071 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1,9 +1,24 @@ export namespace main { + export class ListProcessorsResponse { + name: string; + label: string; + + static createFrom(source: any = {}) { + return new ListProcessorsResponse(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.label = source["label"]; + } + } export class TextSpan { text: string; pos: number; len: number; + append: boolean; static createFrom(source: any = {}) { return new TextSpan(source); @@ -14,6 +29,7 @@ export namespace main { this.text = source["text"]; this.pos = source["pos"]; this.len = source["len"]; + this.append = source["append"]; } } export class ProcessTextRequest { diff --git a/go.mod b/go.mod index 9d0b5a4..47a3602 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,16 @@ module dequoter -go 1.23.3 - -toolchain go1.24.3 +go 1.25 require ( github.com/wailsapp/wails/v2 v2.10.2 - lmika.dev/pkg/modash v0.0.0-20250729120720-cdaa1c8abbe3 + gopkg.in/yaml.v3 v3.0.1 + lmika.dev/pkg/modash v0.1.0 + ucl.lmika.dev v0.1.2 ) require ( + github.com/alecthomas/participle/v2 v2.1.1 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect diff --git a/go.sum b/go.sum index ea6cc63..7eee4e2 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -10,8 +16,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -34,6 +44,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -77,7 +89,12 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -lmika.dev/pkg/modash v0.0.0-20250729120720-cdaa1c8abbe3 h1:EqYZdM6ui++F2NPdFCjKAboOr14eVTIIiRfngBgg/WA= -lmika.dev/pkg/modash v0.0.0-20250729120720-cdaa1c8abbe3/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI= +lmika.dev/pkg/modash v0.1.0 h1:fltroSvP0nKj9K0E6G+S9LULvB9Qhj47+SZ2b9v/v/c= +lmika.dev/pkg/modash v0.1.0/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI= +ucl.lmika.dev v0.1.2 h1:dTqLKGw/pPqE7UrkrJd5qPu2i6BTDzJLaM0cRkJGn6A= +ucl.lmika.dev v0.1.2/go.mod h1:f5RzeCTyBO+4k6LYFuDkwGRujnj4/4ONM60AEtQj02k= diff --git a/models.go b/models.go index 06f33cf..3734a75 100644 --- a/models.go +++ b/models.go @@ -1,9 +1,10 @@ package main type TextSpan struct { - Text string `json:"text"` - Pos int `json:"pos"` - Len int `json:"len"` + Text string `json:"text"` + Pos int `json:"pos"` + Len int `json:"len"` + Append bool `json:"append"` } type ProcessTextRequest struct { @@ -11,6 +12,16 @@ type ProcessTextRequest struct { Input []TextSpan `json:"input"` } +type ListProcessorsResponse struct { + Name string `json:"name"` + Label string `json:"label"` +} + +type SetStatusbarMessage struct { + Message string `json:"message"` + Error bool `json:"error"` +} + type ProcessTextResponse struct { Output []TextSpan `json:"output"` } diff --git a/textfilters.go b/textfilters.go index 10fcf53..5381747 100644 --- a/textfilters.go +++ b/textfilters.go @@ -3,44 +3,185 @@ package main import ( "bufio" "bytes" + "context" "encoding/json" + "errors" + "fmt" "strconv" "strings" + + "gopkg.in/yaml.v3" + "ucl.lmika.dev/ucl" ) -type TextFilter func(input string) (output string, err error) +type TextProcessor struct { + Label string + Description string -var TextFilters = map[string]TextFilter{ - "upper-case": func(input string) (output string, err error) { - return strings.ToUpper(input), nil - }, - "lower-case": func(input string) (output string, err error) { - return strings.ToLower(input), nil - }, - "unquote": func(input string) (output string, err error) { - return strconv.Unquote(input) - }, - "format-json": func(input string) (output string, err error) { - var dst bytes.Buffer + // Filters the supplied text. This is used for manipulating the text input. + Filter TextFilter - scnr := bufio.NewScanner(strings.NewReader(input)) - for scnr.Scan() { - line := scnr.Text() - if err := json.Indent(&dst, []byte(line), "", " "); err == nil { - dst.WriteString("\n") - } else { + // Analyses the supplied text. This is used for extracting information from the text input. The result + // will be displayed in the status bar. Multiple selected regions will be returned as a single string separated by line numbers. + Analyze TextAnalyzer +} + +type TextFilter func(ctx context.Context, input string) (resp TextFilterResponse, err error) +type TextAnalyzer func(ctx context.Context, input string) (resp string, err error) + +type TextFilterResponse struct { + Output string + Append bool +} + +var TextFilters = map[string]TextProcessor{ + "upper-case": { + Label: "String: To Upper Case", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + return TextFilterResponse{Output: strings.ToUpper(input)}, nil + }, + }, + "lower-case": { + Label: "String: To Lower Case", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + return TextFilterResponse{Output: strings.ToLower(input)}, nil + }, + }, + "unquote": { + Label: "String: Unquote", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + out, err := strconv.Unquote(input) + if err != nil { + return TextFilterResponse{}, err + } + return TextFilterResponse{Output: out}, nil + }, + }, + "join-lines-with-commas": { + Label: "Lines: Join with Commas", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + return TextFilterResponse{Output: strings.Replace(input, "\n", ",", -1)}, nil + }, + }, + "split-lines-on-commas": { + Label: "Lines: Split on Commas", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + return TextFilterResponse{Output: strings.Replace(input, ",", "\n", -1)}, nil + }, + }, + "trim-space": { + Label: "Lines: Trim Spaces", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + lines := strings.Split(input, "\n") + for i, line := range lines { + lines[i] = strings.TrimSpace(line) + } + result := strings.Join(lines, "\n") + return TextFilterResponse{Output: result}, nil + }, + }, + "format-json": { + Label: "JSON: Format", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + var dst bytes.Buffer + + scnr := bufio.NewScanner(strings.NewReader(input)) + for scnr.Scan() { + line := scnr.Text() + if err := json.Indent(&dst, []byte(line), "", " "); err == nil { + dst.WriteString("\n") + } else { + return TextFilterResponse{}, err + } + } + + return TextFilterResponse{Output: dst.String()}, nil + }, + }, + "convert-json-to-yaml": { + Label: "Convert: JSON to YAML", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + var data interface{} + if err := json.Unmarshal([]byte(input), &data); err != nil { + return TextFilterResponse{}, err + } + + var dst bytes.Buffer + if err := yaml.NewEncoder(&dst).Encode(data); err != nil { + return TextFilterResponse{}, err + } + + return TextFilterResponse{Output: dst.String()}, nil + }, + }, + "convert-yaml-to-json": { + Label: "Convert: YAML to JSON", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + var data interface{} + if err := yaml.Unmarshal([]byte(input), &data); err != nil { + return TextFilterResponse{}, err + } + + jsonBytes, err := json.MarshalIndent(data, "", " ") + if err != nil { + return TextFilterResponse{}, err + } + + return TextFilterResponse{Output: string(jsonBytes)}, nil + }, + }, + "lorem-ipsum": { + Label: "Generate: Lorem Ipsum", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + return TextFilterResponse{ + Output: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud " + + "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " + + "dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + + "mollit anim id est laborum.", + Append: true, + }, nil + }, + }, + + "lines": { + Label: "Lines: Count", + Analyze: func(ctx context.Context, input string) (result string, err error) { + lineCount := len(strings.Split(input, "\n")) + return fmt.Sprintf("Lines = %v", lineCount), nil + }, + }, + + "ucl-evaluate": { + Label: "UCL: Evaluate", + Description: "Evaluates the input as a UCL expression and displays the result in the status bar.", + Analyze: func(ctx context.Context, input string) (result string, err error) { + res, err := uclInstFromContext(ctx).EvalString(ctx, input) + if err != nil { + if errors.Is(err, ucl.ErrNotConvertable) { + return "Evaluated successfully. No result.", err + } return "", err } - } - return dst.String(), nil + return fmt.Sprintf("Result: %v", res), nil + }, }, - "lorem-ipsum": func(input string) (output string, err error) { - return "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + - "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud " + - "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " + - "dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + - "mollit anim id est laborum.", nil + + "ucl-replace": { + Label: "UCL: Replace", + Description: "Evaluates the input as a UCL expression and replaces it with the result.", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + res, err := uclInstFromContext(ctx).EvalString(ctx, input) + if err != nil { + if errors.Is(err, ucl.ErrNotConvertable) { + return TextFilterResponse{}, err + } + return TextFilterResponse{}, err + } + + return TextFilterResponse{Output: fmt.Sprint(res)}, nil + }, }, }