Skip to content
This repository has been archived by the owner on Jul 9, 2020. It is now read-only.

Implement up/down scrolling (CTRL-E, CTRL-Y, CTRL-U, CTRL-D) #58

Merged
merged 29 commits into from
Oct 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fc98658
Stop view model from directly invoking functions on Buffer module
Kazark Jul 29, 2015
5e8abae
Rename NormalMode module -> NormalModeBindings
Kazark Jul 29, 2015
2f5bec3
Some faltering steps toward CTRL-E/CTRL-Y
Kazark Jul 30, 2015
51c9f58
BROKEN though compiling - step towards fixing request/response
Kazark Jul 30, 2015
4d32367
Turn on warning as error for all projects
Kazark Jul 30, 2015
14632f9
Rename Bus -> BusImpl; SubscribeToBus -> Bus
Kazark Jul 30, 2015
f39063e
NOT COMPILING - wire in new request/response API
Kazark Jul 30, 2015
7d6dd5a
STILL RED - fix compile errors and most unit tests
Kazark Aug 3, 2015
259d8b3
GREEN! - final fixes
Kazark Aug 3, 2015
e783524
First pass at packaged request/response
Kazark Aug 3, 2015
28f5ea2
Add request/response for "get window contents"
Kazark Aug 6, 2015
786f68e
Decouple rendering of command bar from view model service
Kazark Aug 13, 2015
f436be4
Bust the view model apart more
Kazark Aug 20, 2015
23edd51
GREEN BUT BROKEN - continue to bust up view model
Kazark Aug 20, 2015
f2d1195
Fix: window was not drawing
Kazark Aug 21, 2015
9fd66a2
Fix: window not redrawing
Kazark Aug 21, 2015
8701ef8
At last, initial scrolling functionality!
Kazark Aug 21, 2015
c2e8de3
Fix the long-broken Travis build
Kazark Aug 21, 2015
bda47a2
Merge branch 'master' into feature/ctrl-e-ctrl-y
Kazark Aug 22, 2015
17845d6
Add test which uncovered bug in Window logic; fix
Kazark Aug 22, 2015
0bf0c0e
Test-drive some bounds checking for the scrolling
Kazark Aug 27, 2015
075a318
Support CTRL-D & CTRL-U scroll down/up half screen
Kazark Aug 27, 2015
596c030
Refactor to remove duplication in scrolling logic
Kazark Aug 27, 2015
e617b86
Refactor scrolling specs for readability before writing more
Kazark Sep 3, 2015
7310e74
My tests are vacuous. Grr. Committing before I begin to get in too deep.
Kazark Sep 3, 2015
66584d4
Break down the bus implementation with composition
Kazark Sep 3, 2015
854f952
Make all messaging system types private & only expose Bus interface
Kazark Sep 3, 2015
89284af
Made vacuous specs meaningful, which made them fail; got the failing …
Kazark Sep 10, 2015
ac3fd3a
Build the whole solution, not just .fsproj files
Kazark Sep 11, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Target "RestorePackages" (fun _ ->
)

Target "Compile" (fun _ ->
!! "src/**/*.fsproj"
!! "src/Void.sln"
|> MSBuildRelease buildDir "Build"
|> Log "Compile-Output: "
)
Expand Down
65 changes: 34 additions & 31 deletions src/Void.Core.Spec/CommandModeSpec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ type ``Editing command mode``() =
let enter = TextOrHotKey.HotKey HotKey.Enter
let escape = TextOrHotKey.HotKey HotKey.Escape
let backspace = TextOrHotKey.HotKey HotKey.Backspace
let requestSenderStub = CannedResponseRequestSender()

let typeIncrement increment buffer expected =
TextOrHotKey.Text increment
|> CommandMode.handle buffer
|> CommandMode.handle requestSenderStub buffer
|> should equal (expected, CommandMode.Event.TextAppended increment :> Message)

[<SetUp>]
member x.``Set up``() =
requestSenderStub.reset()

[<Test>]
member x.``Text can be incrementally typed in``() =
typeIncrement "e" "" "e"
Expand All @@ -27,49 +32,47 @@ type ``Editing command mode``() =
typeIncrement "t" "edi" "edit"

[<Test>]
member x.``When enter is pressed, the current language for command mode is requested``() =
CommandMode.handle "edit" enter
|> should equal ("edit", GetCurrentCommandLanguageRequest :> Message)
member x.``When enter is pressed, and the current language response comes back, then the fragment interpretation request is sent for that language``() =
requestSenderStub.registerResponse { CurrentCommandLanguage = "python3" }
CommandMode.handle requestSenderStub "edit" enter |> ignore
requestSenderStub.tryPickRequest<InterpretScriptFragmentRequest>()
|> should equal (Some { Language = "python3"; Fragment = "edit"})

[<Test>]
member x.``When enter is pressed, and no response comes back for the current language, then the fragment interpretation request is sent with the default language``() =
CommandMode.handle requestSenderStub "edit" enter |> ignore
requestSenderStub.tryPickRequest<InterpretScriptFragmentRequest>()
|> should equal (Some { Language = "VoidScript"; Fragment = "edit"})

[<Test>]
member x.``When the command text is parsed successfully, the command text is reset``() =
requestSenderStub.registerResponse success
CommandMode.handle requestSenderStub "edit" enter
|> should equal ("", CommandMode.Event.CommandCompleted "edit" :> Message)

[<Test>]
member x.``When the current language is received, the command is interpreted for that language``() =
let command = ref "edit"
CommandMode.handleGetCurrentCommandLanguageResponse command { CurrentCommandLanguage = "python3" }
|> should equal ({ Language = "python3"; Fragment = "edit" } :> Message)
member x.``When the command text is not parsed successfully, the command text is reset``() =
requestSenderStub.registerResponse parseFailure
CommandMode.handle requestSenderStub "edit" enter
|> should equal ("", CoreEvent.ErrorOccurred error :> Message)

[<Test>]
member x.``When there is no response to the request for the current language, the command is interpreted as VoidScript``() =
let command = ref "edit"
CommandMode.handleNoResponseToGetCurrentCommandLanguage command { Request = GetCurrentCommandLanguageRequest }
|> should equal ({ Language = "VoidScript"; Fragment = "edit" } :> Message)
member x.``When the command text parse is incomplete, a newline is added to the command text``() =
requestSenderStub.registerResponse parseIncomplete
CommandMode.handle requestSenderStub "edit" enter
|> should equal ("edit" + System.Environment.NewLine, CommandMode.Event.NewlineAppended :> Message)

[<Test>]
member x.``When escape is pressed, command entry is cancelled``() =
CommandMode.handle "edit" escape
CommandMode.handle requestSenderStub "edit" escape
|> should equal ("", CommandMode.Event.EntryCancelled :> Message)

[<Test>]
member x.``When backspace is pressed, the previous character is remove from the buffer``() =
CommandMode.handle "edig" backspace
CommandMode.handle requestSenderStub "edig" backspace
|> should equal ("edi", CommandMode.Event.CharacterBackspaced :> Message)

[<Test>]
member x.``When backspace is pressed and there are no characters but the prompt, command entry is cancelled``() =
CommandMode.handle "" backspace
CommandMode.handle requestSenderStub "" backspace
|> should equal ("", CommandMode.Event.EntryCancelled :> Message)

[<Test>]
member x.``When the command text is parsed successfully, the command text is reset``() =
CommandMode.handleInterpretFragmentResponse "edit" success
|> should equal ("", CommandMode.Event.CommandCompleted "edit" :> Message)

[<Test>]
member x.``When the command text is not parsed successfully, the command text is reset``() =
CommandMode.handleInterpretFragmentResponse "edit" parseFailure
|> should equal ("", CoreEvent.ErrorOccurred error :> Message)

[<Test>]
member x.``When the command text parse is incomplete, a newline is added to the command text``() =
CommandMode.handleInterpretFragmentResponse "edit" parseIncomplete
|> fst
|> should equal ("edit" + System.Environment.NewLine)
2 changes: 1 addition & 1 deletion src/Void.Core.Spec/NormalModeSpec.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Void.Core.Spec

open Void.Core
open Void.Core.NormalMode
open Void.Core.NormalModeBindings
open NUnit.Framework
open FsUnit

Expand Down
4 changes: 4 additions & 0 deletions src/Void.Core.Spec/Void.Core.Spec.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<WarningLevel>3</WarningLevel>
<DocumentationFile>bin\Debug\Void.Core.Spec.XML</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
Expand All @@ -33,6 +35,8 @@
<DefineConstants>TRACE</DefineConstants>
<WarningLevel>3</WarningLevel>
<DocumentationFile>bin\Release\Void.Core.Spec.XML</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
</PropertyGroup>
<PropertyGroup>
<MinimumVisualStudioVersion Condition="'$(MinimumVisualStudioVersion)' == ''">11</MinimumVisualStudioVersion>
Expand Down
45 changes: 26 additions & 19 deletions src/Void.Core/BaseMessageTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,43 @@ type Message = interface end
type CommandMessage = inherit Message
type EventMessage = inherit Message

type EnvelopeMessage<'TInnerMessage when 'TInnerMessage :> Message> = inherit Message

type RequestMessage = inherit Message
type ResponseMessage<'TRequest when 'TRequest :> RequestMessage> = inherit Message

[<AutoOpen>]
module ``This module is auto-opened to provide a null message`` =
type NoMessage =
| NoMessage
interface Message
type NoMessage = NoMessage interface Message
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, was this picked up by a warning?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No... not sure why I did that actually. Stylistic thrashing I guess. :)

let noMessage = NoMessage :> Message

type NoResponseToRequest<'TRequest when 'TRequest :> RequestMessage> =
{
Request : 'TRequest
}
interface Message

type Handle<'TMsg when 'TMsg :> Message> =
'TMsg -> Message

type HandleRequest<'TRequest when 'TRequest :> RequestMessage> =
'TRequest -> ResponseMessage<'TRequest>
type HandleRequest<'TRequest, 'TResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest>> =
'TRequest -> 'TResponse

type MaybeHandleRequest<'TRequest, 'TResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest>> =
'TRequest -> 'TResponse option

type HandlePackagedRequest<'TRequest, 'TResponse, 'TPackagedRequest, 'TPackagedResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest> and 'TPackagedRequest :> EnvelopeMessage<'TRequest> and 'TPackagedResponse :> EnvelopeMessage<'TResponse>> =
'TPackagedRequest -> 'TPackagedResponse

type MaybeHandlePackagedRequest<'TRequest, 'TResponse, 'TPackagedRequest, 'TPackagedResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest> and 'TPackagedRequest :> EnvelopeMessage<'TRequest> and 'TPackagedResponse :> EnvelopeMessage<'TResponse>> =
'TPackagedRequest -> 'TPackagedResponse option

type MaybeHandleRequest<'TRequest when 'TRequest :> RequestMessage> =
'TRequest -> ResponseMessage<'TRequest> option
type RequestSender =
abstract member makeRequest<'TRequest, 'TResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest>> : 'TRequest -> 'TResponse option

type HandleResponse<'TRequest, 'TResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest>> =
'TResponse -> Message
type PackagedRequestSender =
abstract member makePackagedRequest<'TRequest, 'TResponse, 'TPackagedRequest, 'TPackagedResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest> and 'TPackagedRequest :> EnvelopeMessage<'TRequest> and 'TPackagedResponse :> EnvelopeMessage<'TResponse>> : 'TPackagedRequest -> 'TPackagedResponse option

type SubscribeToBus =
type Bus =
abstract member publish : Message -> unit
abstract member subscribe<'TMsg when 'TMsg :> Message> : Handle<'TMsg> -> unit
abstract member subscribeToRequest<'TRequest when 'TRequest :> RequestMessage> : HandleRequest<'TRequest> -> unit
abstract member subscribeToRequest<'TRequest when 'TRequest :> RequestMessage> : MaybeHandleRequest<'TRequest> -> unit
abstract member subscribeToResponse<'TRequest, 'TResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest>> : HandleResponse<'TRequest, 'TResponse> -> unit
abstract member subscribeToRequest<'TRequest, 'TResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest>> : HandleRequest<'TRequest, 'TResponse> -> unit
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these types be unified somehow to prevent overloading?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. Is there a disadvantage to overloading that you are concerned about? We could always name the methods differently I suppose.

abstract member subscribeToRequest<'TRequest, 'TResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest>> : MaybeHandleRequest<'TRequest, 'TResponse> -> unit
abstract member subscribeToPackagedRequest<'TRequest, 'TResponse, 'TPackagedRequest, 'TPackagedResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest> and 'TPackagedRequest :> EnvelopeMessage<'TRequest> and 'TPackagedResponse :> EnvelopeMessage<'TResponse>> : HandlePackagedRequest<'TRequest, 'TResponse, 'TPackagedRequest, 'TPackagedResponse> -> unit
abstract member subscribeToPackagedRequest<'TRequest, 'TResponse, 'TPackagedRequest, 'TPackagedResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest> and 'TPackagedRequest :> EnvelopeMessage<'TRequest> and 'TPackagedResponse :> EnvelopeMessage<'TResponse>> : MaybeHandlePackagedRequest<'TRequest, 'TResponse, 'TPackagedRequest, 'TPackagedResponse> -> unit
inherit RequestSender
inherit PackagedRequestSender
48 changes: 41 additions & 7 deletions src/Void.Core/BufferList.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
namespace Void.Core

// TODO This is naive, obviously
type FileBuffer = private {
Filepath : string option
Contents : string list
CursorPosition : CellGrid.Cell
}

module Buffer =
open CellGrid

Expand All @@ -13,7 +20,7 @@ module Buffer =
{ Filepath = Some path; Contents = contents; CursorPosition = originCell }

let readLines fileBuffer start =
fileBuffer.Contents |> Seq.skip (start - 1) // Line numbers start at 1
fileBuffer.Contents |> Seq.skip ((start - 1<mLine>)/1<mLine>) // Line numbers start at 1

type Buffers = private {
List : Map<int, FileBuffer>
Expand All @@ -36,13 +43,17 @@ module BufferList =
List = bufferList.List.Add(id, buffer)
LastId = id
}
(listPlusOne, CoreEvent.BufferAdded (id, buffer) :> Message )
let bufferProxy = {
MaybeFilepath = buffer.Filepath
Contents = Seq.ofList buffer.Contents
}
(listPlusOne, { BufferId = id; Message = BufferEvent.Added bufferProxy } :> Message )

let private addEmptyBuffer bufferList =
addBuffer bufferList Buffer.emptyFile

let private writeBufferToPath bufferList bufferId path =
let lines = Buffer.readLines bufferList.List.[bufferId] 0
let lines = Buffer.readLines bufferList.List.[bufferId] 0<mLine>
let msg = Filesystem.Command.SaveToDisk (path, lines) :> Message
(bufferList, msg)

Expand Down Expand Up @@ -72,10 +83,33 @@ module BufferList =
| CoreCommand.WriteBufferToPath (bufferId, path) ->
writeBufferToPath bufferList bufferId path
| _ ->
(bufferList, noMessage)
bufferList, noMessage

let private package bufferId message =
{
BufferId = bufferId
Message = message
}

let handleGetBufferContentsRequest bufferList envelope =
let buffers = (!bufferList).List
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does !bufferList mean?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusingly enough, it means dereference the bufferList reference; i.e. *bufferList in C++ (if I recall my C++). We are using mutable state here, not yet having figured out how to use monads. :)

if buffers.ContainsKey envelope.BufferId
then
let buffer = buffers.[envelope.BufferId]
{
FirstLineNumber = envelope.Message.StartingAtLine
RequestedContents =
if buffer.Contents.Length*1<mLine> < envelope.Message.StartingAtLine
then Seq.empty
else Buffer.readLines buffer envelope.Message.StartingAtLine
}
|> package envelope.BufferId
|> Some
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am becoming quite a fan of |>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YEAH BABY!!

else None

module Service =
let subscribe (subscribeHandler : SubscribeToBus) =
let subscribe (bus : Bus) =
let bufferList = ref empty
subscribeHandler.subscribe <| Service.wrap bufferList handleCommand
subscribeHandler.subscribe <| Service.wrap bufferList handleEvent
Service.wrap bufferList handleCommand |> bus.subscribe
Service.wrap bufferList handleEvent |> bus.subscribe
handleGetBufferContentsRequest bufferList |> bus.subscribeToPackagedRequest
42 changes: 42 additions & 0 deletions src/Void.Core/BufferMessages.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Void.Core

type BufferMessage = inherit Message

type BufferEnvelopeMessage<'TBufferMessage when 'TBufferMessage :> BufferMessage> =
{
BufferId : int
Message : 'TBufferMessage
}
interface EnvelopeMessage<'TBufferMessage>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first concretion on EnvelopeMessage that I'm seeing. I'm curious to see how it's useful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can see it. It's just providing those things that are common to a BufferMessage whereas the BM itself has specifics. Cool!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah my idea is that the BufferListService may ultimately want to be able to handle buffer messages generically, just extract the buffer that is applies to, and delegate to a BufferService which knows how to handle that. There are going to be a lot of buffer-specific messages and I would like to separate the complexity of the buffer list from the actual meat of the buffer logic.

That said, it might be a premature abstraction.


type FileBufferProxy = {
MaybeFilepath : string option
Contents : string seq
}

[<RequireQualifiedAccess>]
type BufferCommand =
| MoveCursor of Motion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

interface CommandMessage
interface BufferMessage

[<RequireQualifiedAccess>]
type BufferEvent =
| Added of FileBufferProxy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means "a file was added to the buffer"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it means "a (file) buffer was added". Maybe I should have spelled out the name more. I was reading it like Buffer(Event).Added. You can't add a file to a buffer. A buffer is created when a file is loaded, and every loaded file has exactly one buffer (ultimately, there may be buffers that do not have files, but a file always has one buffer). You can't load a different file into the same buffer, either; it creates a new buffer.

interface EventMessage
interface BufferMessage

type GetBufferContentsRequest =
{
StartingAtLine : int<mLine>
}
interface RequestMessage
interface BufferMessage

type GetBufferContentsResponse =
{
FirstLineNumber : int<mLine>
RequestedContents : string seq
}
interface ResponseMessage<GetBufferContentsRequest>
interface BufferMessage
38 changes: 38 additions & 0 deletions src/Void.Core/CannedResponseRequestSender.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Void.Core

(* This is really for tests, and probably shouldn't be here.
* However, I'm not going to extract a new assembly for this one class.
* Besides, the test DLL shouldn't depend on Void.Core.
* Void.Core has become unclear anyway: it really should be Void.Base and Void.Editor,
* or something to that effect, and the test DLL should only depend on Void.Base.
* So TODO: when there are several more things like this, create a test library.
* Until then, this code isn't really hurting anybody. *)
type CannedResponseRequestSender() =
let mutable _requests = []
let mutable _responses = []

member x.registerResponse<'TRequest, 'TResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest>> (response : 'TResponse) =
_responses <- box response :: _responses

member x.tryPickRequest<'TRequest when 'TRequest :> RequestMessage>() =
let tryUnbox request =
try
unbox<'TRequest> request |> Some
with _ ->
None
_requests |> List.tryPick tryUnbox

member x.reset() =
_requests <- []
_responses <- []

interface RequestSender with
member x.makeRequest<'TRequest, 'TResponse when 'TRequest :> RequestMessage and 'TResponse :> ResponseMessage<'TRequest>> (request : 'TRequest) =
let tryUnbox response =
try
unbox<'TResponse> response |> Some
with _ ->
None
_requests <- box request :: _requests
_responses |> List.tryPick tryUnbox

10 changes: 4 additions & 6 deletions src/Void.Core/CommandHistory.fs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,10 @@ module CommandHistory =
move previous history
| CommandHistoryCommand.MoveToNextCommand ->
move next history
| _ ->
(history, noMessage)

module Service =
let subscribe (subscribeHandler : SubscribeToBus) =
let subscribe (bus : Bus) =
let history = ref empty
subscribeHandler.subscribe <| Service.wrap history handleEvent
subscribeHandler.subscribe <| Service.wrap history handleCommand
subscribeHandler.subscribe <| Service.wrap history handleCoreEvent
bus.subscribe <| Service.wrap history handleEvent
bus.subscribe <| Service.wrap history handleCommand
bus.subscribe <| Service.wrap history handleCoreEvent
Loading