diff --git a/src/pocof.Test/PocofAction.fs b/src/pocof.Test/PocofAction.fs index 6bfd1930..2eeccb73 100644 --- a/src/pocof.Test/PocofAction.fs +++ b/src/pocof.Test/PocofAction.fs @@ -39,18 +39,18 @@ module ``toKeyPattern should returns`` = module ``get should returns`` = [] - let ``PocofData.AddChar if no modifier is specified.`` () = + let ``PocofData.AddQuery if no modifier is specified.`` () = let key = [ new ConsoleKeyInfo('a', ConsoleKey.A, false, false, false) ] let actual = PocofAction.get Map.empty key - actual |> shouldEqual (PocofData.AddChar "a") + actual |> shouldEqual (PocofData.AddQuery "a") [] - let ``PocofData.AddChar if symbol with shift.`` () = + let ``PocofData.AddQuery if symbol with shift.`` () = let getKey = [ new ConsoleKeyInfo(':', ConsoleKey.Oem1, true, false, false) ] let actual = PocofAction.get Map.empty getKey - actual |> shouldEqual (PocofData.AddChar ":") + actual |> shouldEqual (PocofData.AddQuery ":") [] let ``user-defined Action if matched.`` () = @@ -99,14 +99,14 @@ module ``get should returns`` = actual |> shouldEqual PocofData.BeginningOfLine [] - let ``PocofData.AddChar if not match the keymap.`` () = + let ``PocofData.AddQuery if not match the keymap.`` () = let keyMap: Map = Map [ ({ Modifier = 1; Key = ConsoleKey.U }, PocofData.KillBeginningOfLine) ] let key = [ new ConsoleKeyInfo('u', ConsoleKey.U, false, true, true) ] let actual = PocofAction.get keyMap key - actual |> shouldEqual (PocofData.AddChar "u") + actual |> shouldEqual (PocofData.AddQuery "u") [] let ``PocofData.None if the control character not match the keymap.`` () = diff --git a/src/pocof.Test/PocofData.fs b/src/pocof.Test/PocofData.fs index 57933e02..1121e395 100644 --- a/src/pocof.Test/PocofData.fs +++ b/src/pocof.Test/PocofData.fs @@ -14,14 +14,14 @@ module ``Action fromString should returns`` = |> shouldEqual (Error "Unknown case 'XXX'.") [] - let ``Error when AddChar.`` () = - Action.fromString "AddChar" - |> shouldEqual (Error "Unknown case 'AddChar'.") + let ``Error when AddQuery.`` () = + Action.fromString "AddQuery" + |> shouldEqual (Error "Unknown case 'AddQuery'.") [] - let ``known actions excluding AddChar.`` () = + let ``known actions excluding AddQuery.`` () = FSharpType.GetUnionCases(typeof) - |> Seq.filter (fun a -> a.Name <> "AddChar") + |> Seq.filter (fun a -> a.Name <> "AddQuery") |> Seq.iter (fun a -> [ a.Name String.lower a.Name @@ -235,10 +235,10 @@ module invokeAction = let position: Position = { X = 0; Y = 0 } - module ``with AddChar`` = + module ``with AddQuery`` = [] let ``should return a property search state and position.x = 1 when the char is colon.`` () = - invokeAction state { X = 0; Y = 0 } (AddChar ":") + invokeAction state { X = 0; Y = 0 } [] (AddQuery ":") |> shouldEqual ( { state with Query = ":" @@ -254,7 +254,8 @@ module invokeAction = Query = ":name" PropertySearch = Search "name" } { X = 5; Y = 0 } - (AddChar " ") + [] + (AddQuery " ") |> shouldEqual ( { state with Query = ":name " @@ -273,7 +274,7 @@ module invokeAction = let position: Position = { X = 0; Y = 0 } - invokeAction state position BackwardChar + invokeAction state position [] BackwardChar |> shouldEqual (state, position, NotRequired) [] @@ -283,6 +284,7 @@ module invokeAction = Query = ":name" PropertySearch = Search "name" } { X = 5; Y = 0 } + [] BackwardChar |> shouldEqual ( { state with @@ -300,6 +302,7 @@ module invokeAction = Query = ":name" PropertySearch = Search "" } { X = 1; Y = 0 } + [] ForwardChar |> shouldEqual ( { state with @@ -318,6 +321,7 @@ module invokeAction = Query = ":name" PropertySearch = Search "name" } { X = 5; Y = 0 } + [] ForwardChar |> shouldEqual ( { state with @@ -335,6 +339,7 @@ module invokeAction = Query = ":name" PropertySearch = Search "name" } { X = 5; Y = 0 } + [] BeginningOfLine |> shouldEqual ( { state with @@ -351,6 +356,7 @@ module invokeAction = Query = ":name" PropertySearch = Search "name" } { X = 0; Y = 0 } + [] BeginningOfLine |> shouldEqual ( { state with @@ -367,6 +373,7 @@ module invokeAction = Query = "query" PropertySearch = NonSearch } { X = 5; Y = 0 } + [] BeginningOfLine |> (shouldEqual ( { state with @@ -384,6 +391,7 @@ module invokeAction = Query = ":name" PropertySearch = Search "n" } { X = 2; Y = 0 } + [] EndOfLine |> shouldEqual ( { state with @@ -400,6 +408,7 @@ module invokeAction = Query = ":name" PropertySearch = Search "name" } { X = 5; Y = 0 } + [] EndOfLine |> shouldEqual ( { state with @@ -416,6 +425,7 @@ module invokeAction = Query = "query" PropertySearch = NonSearch } { X = 0; Y = 0 } + [] EndOfLine |> shouldEqual ( { state with @@ -433,6 +443,7 @@ module invokeAction = Query = ":name " PropertySearch = NonSearch } { X = 6; Y = 0 } + [] DeleteBackwardChar |> shouldEqual ( { state with @@ -449,6 +460,7 @@ module invokeAction = Query = ":name" PropertySearch = NonSearch } { X = 0; Y = 0 } + [] DeleteBackwardChar |> shouldEqual ( { state with @@ -466,6 +478,7 @@ module invokeAction = Query = ":name " PropertySearch = Search "name" } { X = 0; Y = 0 } + [] DeleteForwardChar |> shouldEqual ( { state with @@ -482,6 +495,7 @@ module invokeAction = Query = ":name" PropertySearch = Search "name" } { X = 5; Y = 0 } + [] DeleteForwardChar |> shouldEqual ( { state with @@ -494,27 +508,27 @@ module invokeAction = module ``with KillBeginningOfLine`` = [] let ``should remove all characters before the specified position.`` () = - invokeAction { state with Query = "examplequery" } { X = 7; Y = 0 } KillBeginningOfLine + invokeAction { state with Query = "examplequery" } { X = 7; Y = 0 } [] KillBeginningOfLine |> shouldEqual ({ state with Query = "query" }, { X = 0; Y = 0 }, Required) [] let ``should not change state if the cursor position is at the begin of line.`` () = - invokeAction { state with Query = "query" } { X = 0; Y = 0 } KillBeginningOfLine + invokeAction { state with Query = "query" } { X = 0; Y = 0 } [] KillBeginningOfLine |> shouldEqual ({ state with Query = "query" }, { X = 0; Y = 0 }, NotRequired) module ``with KillEndOfLine`` = [] let ``should remove characters after the current cursor position.`` () = - invokeAction { state with Query = "examplequery" } { X = 7; Y = 0 } KillEndOfLine + invokeAction { state with Query = "examplequery" } { X = 7; Y = 0 } [] KillEndOfLine |> shouldEqual ({ state with Query = "example" }, { X = 7; Y = 0 }, Required) [] let ``should not change state if the cursor position is at the end of line.`` () = - invokeAction { state with Query = "example" } { X = 7; Y = 0 } KillEndOfLine + invokeAction { state with Query = "example" } { X = 7; Y = 0 } [] KillEndOfLine |> shouldEqual ({ state with Query = "example" }, { X = 7; Y = 0 }, NotRequired) let testStateOnly action state expected = - invokeAction state position action + invokeAction state position [] action |> shouldEqual (expected, position, Required) module ``with RotateMatcher`` = @@ -589,25 +603,161 @@ module invokeAction = let ``should return a disabled suppress property.`` () = test true false let noop action = - invokeAction state position action + invokeAction state position [] action |> shouldEqual (state, position, NotRequired) // TODO: change to false when the action is implemented. module ``with SelectUp`` = [] - let ``should return any difference when a up-arrow is entered.`` () = noop SelectUp + let ``shouldn't return any difference when a up-arrow is entered.`` () = noop SelectUp module ``with SelectDown`` = [] - let ``should return any difference when a down-arrow is entered.`` () = noop SelectDown + let ``shouldn't return any difference when a down-arrow is entered.`` () = noop SelectDown module ``with ScrollPageUp`` = [] - let ``should return any difference when a page-up is entered.`` () = noop ScrollPageUp + let ``shouldn't return any difference when a page-up is entered.`` () = noop ScrollPageUp module ``with ScrollPageDown`` = [] - let ``should return any difference when a page-down is entered.`` () = noop ScrollPageDown + let ``shouldn't return any difference when a page-down is entered.`` () = noop ScrollPageDown module ``with TabExpansion`` = [] - let ``should return any difference when a tab is entered.`` () = noop TabExpansion + let ``shouldn't return any difference when a tab is entered with non search mode.`` () = + invokeAction state position [ "name"; "path" ] CompleteProperty + |> shouldEqual (state, position, NotRequired) + + [] + let ``shouldn't return any difference when a tab is entered with empty properties list.`` () = + let state = + { state with + Query = ":" + PropertySearch = Search "" } + + invokeAction state position [] CompleteProperty + |> shouldEqual (state, position, NotRequired) + + [] + let ``shouldn't return any difference when a tab is entered and found no property completion.`` () = + let state = + { state with + Query = ":a" + PropertySearch = Search "a" } + + invokeAction state position [ "name"; "path" ] CompleteProperty + |> shouldEqual (state, position, NotRequired) + + [] + let ``should return completion when a property is found.`` () = + let state = + { state with + Query = ":p" + PropertySearch = Search "p" } + + let position = { position with X = 2 } + + invokeAction state position [ "name"; "path" ] CompleteProperty + |> shouldEqual ( + { state with + Query = ":path" + PropertySearch = Rotate("p", 0, [ "path" ]) }, + { position with X = 5 }, + Required + ) + + [] + let ``should return completion when some properties are found.`` () = + let state = + { state with + Query = ":n" + PropertySearch = Search "n" } + + let position = { position with X = 2 } + + invokeAction state position [ "name"; "path"; "number" ] CompleteProperty + |> shouldEqual ( + { state with + Query = ":name" + PropertySearch = Rotate("n", 0, [ "name"; "number" ]) }, + { position with X = 5 }, + Required + ) + + [] + let ``should insert completion to mid of query when a property is found.`` () = + let state = + { state with + Query = ":n foo" + PropertySearch = Search "n" } + + let position = { position with X = 2 } + + invokeAction state position [ "name"; "path" ] CompleteProperty + |> shouldEqual ( + { state with + Query = ":name foo" + PropertySearch = Rotate("n", 0, [ "name" ]) }, + { position with X = 5 }, + Required + ) + + [] + let ``shouldn't return any difference when a property is already completed.`` () = + let state = + { state with + Query = ":name" + PropertySearch = Search "name" } + + let position = { position with X = 5 } + + invokeAction state position [ "name"; "path" ] CompleteProperty + |> shouldEqual ({ state with PropertySearch = Rotate("name", 0, [ "name" ]) }, position, Required) + + [] + let ``shouldn't return any difference when a property is already completed to mid of query.`` () = + let state = + { state with + Query = ":name a" + PropertySearch = Search "name" } + + let position = { position with X = 5 } + + invokeAction state position [ "name"; "path" ] CompleteProperty + |> shouldEqual ({ state with PropertySearch = Rotate("name", 0, [ "name" ]) }, position, Required) + + [] + let ``should return next property when rotation.`` () = + let state = + { state with + Query = ":name" + PropertySearch = Rotate("n", 0, [ "name"; "number" ]) } + + let position = { position with X = 5 } + + invokeAction state position [ "name"; "path"; "number" ] CompleteProperty + |> shouldEqual ( + { state with + Query = ":number" + PropertySearch = Rotate("n", 1, [ "name"; "number" ]) }, + { position with X = 7 }, + Required + ) + + [] + let ``should return first property when next rotation not found.`` () = + let state = + { state with + Query = ":number" + PropertySearch = Rotate("n", 1, [ "name"; "number" ]) } + + let position = { position with X = 7 } + + invokeAction state position [ "name"; "path"; "number" ] CompleteProperty + |> shouldEqual ( + { state with + Query = ":name" + PropertySearch = Rotate("n", 0, [ "name"; "number" ]) }, + { position with X = 5 }, + Required + ) diff --git a/src/pocof/Action.fs b/src/pocof/Action.fs index 608c27cf..a675fc84 100644 --- a/src/pocof/Action.fs +++ b/src/pocof/Action.fs @@ -48,7 +48,7 @@ module PocofAction = (plain ConsoleKey.PageUp, PocofData.ScrollPageUp) (plain ConsoleKey.PageDown, PocofData.ScrollPageDown) - (plain ConsoleKey.Tab, PocofData.TabExpansion) ] + (plain ConsoleKey.Tab, PocofData.CompleteProperty) ] let inline toEnum<'a when 'a :> Enum and 'a: struct and 'a: (new: unit -> 'a)> (k: string) = match Enum.TryParse<'a>(k, true) with @@ -139,8 +139,8 @@ module PocofAction = (fun acc x -> (acc, x) |> function - | PocofData.AddChar s, Char c -> string c |> (+) s |> PocofData.AddChar - | _, Char c -> PocofData.AddChar <| string c + | PocofData.AddQuery s, Char c -> string c |> (+) s |> PocofData.AddQuery + | _, Char c -> PocofData.AddQuery <| string c | _, Shortcut a -> a | _, Control _ -> PocofData.Noop) PocofData.Noop diff --git a/src/pocof/Data.fs b/src/pocof/Data.fs index 2156c24b..72a3f6fe 100644 --- a/src/pocof/Data.fs +++ b/src/pocof/Data.fs @@ -1,11 +1,47 @@ namespace pocof -open System -open System.Management.Automation -open System.Collections -open Microsoft.FSharp.Reflection +// for debugging. +module PocofDebug = + open System + open System.IO + open System.Runtime.CompilerServices + open System.Runtime.InteropServices + + let lockObj = new obj () + + let logPath = "./debug.log" + + [] + type Logger = + static member logFile + ( + res, + [] caller: string, + [] path: string, + [] line: int + ) = + + // NOTE: lock to avoid another process error when dotnet test. + lock lockObj (fun () -> + use sw = new StreamWriter(logPath, true) + + res + |> List.iter (fun r -> + fprintfn + sw + "[%s] %s at %d %s <%A>" + (DateTimeOffset.Now.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz")) + path + line + caller + r)) module PocofData = + open System + open System.Management.Automation + open System.Collections + open Microsoft.FSharp.Reflection + type Entry = | Obj of PSObject | Dict of DictionaryEntry @@ -54,7 +90,7 @@ module PocofData = | BeginningOfLine | EndOfLine // edit query. - | AddChar of string + | AddQuery of string | DeleteBackwardChar | DeleteForwardChar | KillBeginningOfLine @@ -71,8 +107,10 @@ module PocofData = | ScrollPageUp | ScrollPageDown // autocomplete - | TabExpansion - static member fromString = tryFromStringExcludes <| set [ "AddChar" ] + | CompleteProperty + static member fromString = + tryFromStringExcludes + <| set [ "AddQuery" ] type Matcher = | EQ @@ -96,6 +134,7 @@ module PocofData = type PropertySearch = | NonSearch | Search of string + | Rotate of string * int * string list type Refresh = | Required @@ -297,9 +336,51 @@ module PocofData = let private switchSuppressProperties (state: InternalState) = { state with SuppressProperties = not state.SuppressProperties } - let invokeAction (state: InternalState) (pos: Position) = + let private completeProperty (state: InternalState) (pos: Position) (props: string list) = + let splitQuery keyword = + let basePosition = pos.X - String.length keyword + let head = state.Query.[.. basePosition - 1] + let tail = state.Query.[pos.X ..] + basePosition, head, tail + + let buildValues head next tail keyword i candidates basePosition = + { state with + Query = $"%s{head}%s{next}%s{tail}" + PropertySearch = Rotate(keyword, i, candidates) }, + { pos with X = basePosition + next.Length }, + Required + + match state.PropertySearch with + | NonSearch -> state, pos, NotRequired + | Search keyword -> + let candidate, candidates = + props + |> List.filter (fun p -> p.ToLower().StartsWith(keyword.ToLower())) + |> function + | [] -> "", [] + | xs -> List.head xs, xs + + match candidate with + | "" -> state, pos, NotRequired + | _ -> + let basePosition, head, tail = splitQuery keyword +#if DEBUG + PocofDebug.Logger.logFile [ $"Search keyword '{keyword}' head '{head}' candidate '{candidate}' tail '{tail}'" ] +#endif + buildValues head candidate tail keyword 0 candidates basePosition + | Rotate (keyword, i, candidates) -> + let cur = candidates.[i] + let i = (i + 1) % candidates.Length + let next = candidates.[i] + let basePosition, head, tail = splitQuery cur +#if DEBUG + PocofDebug.Logger.logFile [ $"Rotate keyword '{keyword}' head '{head}' cur '{cur}' next '{next}' tail '{tail}'" ] +#endif + buildValues head next tail keyword i candidates basePosition + + let invokeAction (state: InternalState) (pos: Position) (props: string list) = function - | AddChar s -> addQuery state pos s + | AddQuery s -> addQuery state pos s | BackwardChar -> moveBackward state pos | ForwardChar -> moveForward state pos | BeginningOfLine -> moveHead state pos @@ -317,7 +398,8 @@ module PocofData = | SelectDown -> state, pos, NotRequired // TODO: implement it. | ScrollPageUp -> state, pos, NotRequired // TODO: implement it. | ScrollPageDown -> state, pos, NotRequired // TODO: implement it. - | TabExpansion -> state, pos, NotRequired // TODO: implement it. + // TODO: i think it is not good to include props in invokeAction only for completion. + | CompleteProperty -> completeProperty state pos props | x -> failwithf "unrecognized Action. value='%s'" <| x.GetType().Name diff --git a/src/pocof/Library.fs b/src/pocof/Library.fs index 876044bf..381b2e88 100644 --- a/src/pocof/Library.fs +++ b/src/pocof/Library.fs @@ -79,7 +79,7 @@ type SelectPocofCommand() = | Cancel -> [] | Finish -> unwrap l | Noop -> loop l s pos NotRequired - | a -> invokeAction s pos a |||> loop l + | a -> invokeAction s pos props a |||> loop l loop input state pos Required diff --git a/src/pocof/UI.fs b/src/pocof/UI.fs index 00cd24bb..5b9be48c 100644 --- a/src/pocof/UI.fs +++ b/src/pocof/UI.fs @@ -1,42 +1,5 @@ namespace pocof -// for debugging. -module PocofDebug = - open System - open System.IO - open System.Runtime.CompilerServices - open System.Runtime.InteropServices - - let lockObj = new obj () - - let logPath = "./debug.log" - - [] - type Logger = - static member logFile - ( - res, - [] caller: string, - [] path: string, - [] line: int - ) = - - // NOTE: lock to avoid another process error when dotnet test. - lock lockObj (fun () -> - use sw = new StreamWriter(logPath, true) - - res - |> List.iter (fun r -> - fprintfn - sw - "[%s] %s at %d %s <%A>" - (DateTimeOffset.Now.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz")) - path - line - caller - r)) - - module PocofScreen = open System open System.Management.Automation.Host @@ -170,11 +133,7 @@ module PocofScreen = __.writeScreenLine <| toHeight i <| match List.tryItem i out with - | Some s -> -#if DEBUG - PocofDebug.Logger.logFile [ s ] -#endif - s + | Some s -> s | None -> String.Empty) rui.SetCursorPosition