{-# LANGUAGE DeriveDataTypeable #-}moduleYi.Buffer.TextUnit(TextUnit(..),outsideUnit,leftBoundaryUnit,unitWord,unitViWord,unitViWORD,unitViWordAnyBnd,unitViWORDAnyBnd,unitViWordOnLine,unitViWORDOnLine,unitDelimited,unitSentence,unitEmacsParagraph,unitParagraph,isAnySep,unitSep,unitSepThisLine,isWordChar,moveB,maybeMoveB,transformB,transposeB,regionOfB,regionOfNonEmptyB,regionOfPartB,regionWithTwoMovesB,regionOfPartNonEmptyB,regionOfPartNonEmptyAtB,readPrevUnitB,readUnitB,untilB,doUntilB_,untilB_,whileB,doIfCharB,atBoundaryB,numberOfB,deleteB,genMaybeMoveB,genMoveB,BoundarySide(..),genAtBoundaryB,checkPeekB,halfUnit,deleteUnitB)whereimportControl.ApplicativeimportControl.MonadimportData.TypeableimportData.CharimportYi.Buffer.BasicimportYi.Buffer.MiscimportYi.Buffer.Region-- | Designate a given "unit" of text.dataTextUnit=Character-- ^ a single character|Line-- ^ a line of text (between newlines)|VLine-- ^ a "vertical" line of text (area of text between two characters at the same column number)|Document-- ^ the whole document|GenUnit{genEnclosingUnit::TextUnit,genUnitBoundary::Direction->BufferMBool}-- there could be more text units, like Page, Searched, etc. it's probably a good-- idea to use GenUnit though.derivingTypeable-- | Turns a unit into its "negative" by inverting the boundaries. For example,-- @outsideUnit unitViWord@ will be the unit of spaces between words. For units-- without boundaries ('Character', 'Document', ...), this is the identity-- function.outsideUnit::TextUnit->TextUnitoutsideUnit(GenUnitenclosingboundary)=GenUnitenclosing(boundary.reverseDir)outsideUnitx=x-- for a lack of better definition-- | Common boundary checking function: run the condition on @siz@ characters in specified direction-- shifted by specified offset.genBoundary::Int->(String->Bool)->Direction->BufferMBoolgenBoundaryofsconditiondir=condition<$>peekBwhere-- | read some characters in the specified directionpeekB=savingPointB$domoveN$mayNegateofsfmapsnd<$>(indexedStreamBdir=<<pointB)mayNegate=casedirofForward->idBackward->negate-- | a word as in use in Emacs (fundamental mode)unitWord::TextUnitunitWord=GenUnitDocument$\direction->checkPeekB(-1)[isWordChar,not.isWordChar]direction-- ^ delimited on the left and right by given characters, boolean argument tells if whether those are included.unitDelimited::Char->Char->Bool->TextUnitunitDelimitedleftrightincluded=GenUnitDocument$\direction->case(included,direction)of(False,Backward)->checkPeekB0[(==left)]Backward(False,Forward)->(==right)<$>readB(True,Backward)->checkPeekB(-1)[(==left)]Backward(True,Forward)->checkPeekB0[(==right)]BackwardisWordChar::Char->BoolisWordCharx=isAlphaNumx||x=='_'isNl::Char->BoolisNl=(=='\n')-- | Tells if a char can end a sentence ('.', '!', '?').isEndOfSentence::Char->BoolisEndOfSentence=(`elem`".!?")-- | Verifies that the list matches all the predicates, pairwise.-- If the list is "too small", then return 'False'.checks::[a->Bool]->[a]->Boolchecks[]_=Truechecks_[]=Falsechecks(p:ps)(x:xs)=px&&checkspsxscheckPeekB::Int->[Char->Bool]->Direction->BufferMBoolcheckPeekBoffsetconds=genBoundaryoffset(checksconds)atViWordBoundary::(Char->Int)->Direction->BufferMBoolatViWordBoundarycharType=genBoundary(-1)$\cs->casecsof(c1:c2:_)->isNlc1&&isNlc2-- stop at empty lines||not(isSpacec1)&&(charTypec1/=charTypec2)_->TrueatAnyViWordBoundary::(Char->Int)->Direction->BufferMBoolatAnyViWordBoundarycharType=genBoundary(-1)$\cs->casecsof(c1:c2:_)->isNlc1||isNlc2||charTypec1/=charTypec2_->TrueatViWordBoundaryOnLine::(Char->Int)->Direction->BufferMBoolatViWordBoundaryOnLinecharType=genBoundary(-1)$\cs->casecsof(c1:c2:_)->isNlc1||isNlc2||not(isSpacec1)&&charTypec1/=charTypec2_->TrueunitViWord::TextUnitunitViWord=GenUnitDocument$atViWordBoundaryviWordCharTypeunitViWORD::TextUnitunitViWORD=GenUnitDocument$atViWordBoundaryviWORDCharTypeunitViWordAnyBnd::TextUnitunitViWordAnyBnd=GenUnitDocument$atAnyViWordBoundaryviWordCharTypeunitViWORDAnyBnd::TextUnitunitViWORDAnyBnd=GenUnitDocument$atAnyViWordBoundaryviWORDCharTypeunitViWordOnLine::TextUnitunitViWordOnLine=GenUnitDocument$atViWordBoundaryOnLineviWordCharTypeunitViWORDOnLine::TextUnitunitViWORDOnLine=GenUnitDocument$atViWordBoundaryOnLineviWORDCharTypeviWordCharType::Char->IntviWordCharTypec|isSpacec=1|isWordCharc=2|otherwise=3viWORDCharType::Char->IntviWORDCharTypec|isSpacec=1|otherwise=2-- | Separator characters (space, tab, unicode separators). Most of the units-- above attempt to identify "words" with various punctuation and symbols included-- or excluded. This set of units is a simple inverse: it is true for "whitespace"-- or "separators" and false for anything that is not (letters, numbers, symbols,-- punctuation, whatever).isAnySep::Char->BoolisAnySepc=isSeparatorc||isSpacec||generalCategoryc`elem`[Space,LineSeparator,ParagraphSeparator]atSepBoundary::Direction->BufferMBoolatSepBoundary=genBoundary(-1)$\cs->casecsof(c1:c2:_)->isNlc1||isNlc2||isAnySepc1/=isAnySepc2_->True-- | unitSep is true for any kind of whitespace/separatorunitSep::TextUnitunitSep=GenUnitDocumentatSepBoundary-- | unitSepThisLine is true for any kind of whitespace/separator on this line onlyunitSepThisLine::TextUnitunitSepThisLine=GenUnitLineatSepBoundary-- | Is the point at a @Unit@ boundary in the specified @Direction@?atBoundary::TextUnit->Direction->BufferMBoolatBoundaryDocumentBackward=(==0)<$>pointBatBoundaryDocumentForward=(>=)<$>pointB<*>sizeBatBoundaryCharacter_=returnTrueatBoundaryVLine_=returnTrue-- a fallacy; this needs a little refactoring.atBoundaryLinedirection=checkPeekB0[isNl]directionatBoundary(GenUnit_atBound)dir=atBounddirenclosingUnit::TextUnit->TextUnitenclosingUnit(GenUnitenclosing_)=enclosingenclosingUnit_=DocumentatBoundaryB::TextUnit->Direction->BufferMBoolatBoundaryBDocumentd=atBoundaryDocumentdatBoundaryBud=(||)<$>atBoundaryud<*>atBoundaryB(enclosingUnitu)d-- | Paragraph to implement emacs-like forward-paragraph/backward-paragraphunitEmacsParagraph::TextUnitunitEmacsParagraph=GenUnitDocument$checkPeekB(-2)[not.isNl,isNl,isNl]-- | Paragraph that begins and ends in the paragraph, not the empty lines surrounding it.unitParagraph::TextUnitunitParagraph=GenUnitDocument$checkPeekB(-1)[not.isNl,isNl,isNl]unitSentence::TextUnitunitSentence=GenUnitunitEmacsParagraph$\dir->checkPeekB(ifdir==Forwardthen-1else0)(mayReversedir[isEndOfSentence,isSpace])dir-- | Unit that have its left and right boundaries at the left boundary of the argument unit.leftBoundaryUnit::TextUnit->TextUnitleftBoundaryUnitu=GenUnitDocument(\_dir->atBoundaryBuBackward)-- | @genAtBoundaryB u d s@ returns whether the point is at a given boundary @(d,s)@ .-- Boundary @(d,s)@ , taking Word as example, means:-- Word-- ^^ ^^-- 12 34-- 1: (Backward,OutsideBound)-- 2: (Backward,InsideBound)-- 3: (Forward,InsideBound)-- 4: (Forward,OutsideBound)---- rules:-- genAtBoundaryB u Backward InsideBound = atBoundaryB u Backward-- genAtBoundaryB u Forward OutsideBound = atBoundaryB u ForwardgenAtBoundaryB::TextUnit->Direction->BoundarySide->BufferMBoolgenAtBoundaryBuds=withOffset(offuds)$atBoundaryBudwherewithOffset0f=fwithOffsetofsf=savingPointB(((ofs+)<$>pointB)>>=moveTo>>f)off_BackwardInsideBound=0off_BackwardOutsideBound=1off_ForwardInsideBound=1off_ForwardOutsideBound=0numberOfB::TextUnit->TextUnit->BufferMIntnumberOfBunitcontainingUnit=savingPointB$domaybeMoveBcontainingUnitBackwardstart<-pointBmoveBcontainingUnitForwardend<-pointBmoveTostartlength<$>untilB((>=end)<$>pointB)(moveBunitForward)whileB::BufferMBool->BufferMa->BufferM[a]whileBcond=untilB(not<$>cond)-- | Repeat an action until the condition is fulfilled or the cursor stops moving.-- The Action may be performed zero times.untilB::BufferMBool->BufferMa->BufferM[a]untilBcondf=dostop<-condifstopthenreturn[]elsedoUntilBcondf-- | Repeat an action until the condition is fulfilled or the cursor stops moving.-- The Action is performed at least once.doUntilB::BufferMBool->BufferMa->BufferM[a]doUntilBcondf=loopwhereloop=dop<-pointBx<-fp'<-pointBstop<-cond(x:)<$>ifp/=p'&&notstopthenloopelsereturn[]doUntilB_::BufferMBool->BufferMa->BufferM()doUntilB_condf=void(doUntilBcondf)-- maybe do an optimized version?untilB_::BufferMBool->BufferMa->BufferM()untilB_condf=void(untilBcondf)-- maybe do an optimized version?-- | Do an action if the current buffer character passes the predicatedoIfCharB::(Char->Bool)->BufferMa->BufferM()doIfCharBpo=readB>>=\c->when(pc)$voido-- | Boundary sidedataBoundarySide=InsideBound|OutsideBoundderivingEq-- | Generic move operation-- Warning: moving To the (OutsideBound, Backward) bound of Document is impossible (offset -1!)-- @genMoveB u b d@: move in direction d until encountering boundary b or unit u. See 'genAtBoundaryB' for boundary explanation.genMoveB::TextUnit->(Direction,BoundarySide)->Direction->BufferM()genMoveBDocument(Forward,InsideBound)Forward=moveTo=<<subtract1<$>sizeBgenMoveBDocument_Forward=moveTo=<<sizeBgenMoveBDocument_Backward=moveTo0-- impossible to go outside beginning of doc.genMoveBCharacter_Forward=rightBgenMoveBCharacter_Backward=leftBgenMoveBVLine_Forward=doofs<-lineMoveRel1when(ofs<1)(maybeMoveBLineForward)genMoveBVLine_Backward=lineUpgenMoveBunit(boundDir,boundSide)moveDir=doUntilB_(genAtBoundaryBunitboundDirboundSide)(moveBCharactermoveDir)-- | Generic maybe move operation.-- As genMoveB, but don't move if we are at boundary already.genMaybeMoveB::TextUnit->(Direction,BoundarySide)->Direction->BufferM()genMaybeMoveBDocumentboundSpecmoveDir=genMoveBDocumentboundSpecmoveDir-- optimized case for DocumentgenMaybeMoveBLine(Backward,InsideBound)Backward=moveTo=<<solPointB=<<pointB-- optimized case for begin of LinegenMaybeMoveBunit(boundDir,boundSide)moveDir=untilB_(genAtBoundaryBunitboundDirboundSide)(moveBCharactermoveDir)-- | Move to the next unit boundarymoveB::TextUnit->Direction->BufferM()moveBud=genMoveBu(d,casedofForward->OutsideBound;Backward->InsideBound)d-- | As 'moveB', unless the point is at a unit boundary-- So for example here moveToEol = maybeMoveB Line Forward;-- in that it will move to the end of current line and nowhere if we-- are already at the end of the current line. Similarly for moveToSol.maybeMoveB::TextUnit->Direction->BufferM()maybeMoveBud=genMaybeMoveBu(d,casedofForward->OutsideBound;Backward->InsideBound)dtransposeB::TextUnit->Direction->BufferM()transposeBunitdirection=domoveBunit(reverseDirdirection)w0<-pointBmoveBunitdirectionw0'<-pointBmoveBunitdirectionw1'<-pointBmoveBunit(reverseDirdirection)w1<-pointBswapRegionsB(mkRegionw0w0')(mkRegionw1w1')moveTow1'transformB::(String->String)->TextUnit->Direction->BufferM()transformBfunitdirection=dop<-pointBmoveBunitdirectionq<-pointBletr=mkRegionpqreplaceRegionBr=<<f<$>readRegionBr-- | Delete between point and next unit boundary, return the deleted region.deleteB::TextUnit->Direction->BufferM()deleteBunitdir=deleteRegionB=<<regionOfPartNonEmptyBunitdirregionWithTwoMovesB::BufferMa->BufferMb->BufferMRegionregionWithTwoMovesBmove1move2=savingPointB$mkRegion<$>(move1>>pointB)<*>(move2>>pointB)-- | Region of the whole textunit where the current point is.regionOfB::TextUnit->BufferMRegionregionOfBunit=regionWithTwoMovesB(maybeMoveBunitBackward)(maybeMoveBunitForward)-- An alternate definition would be the following, but it can return two units if the current point is between them.-- eg. "word1 ^ word2" would return both words.-- regionOfB unit = mkRegion-- <$> pointAfter (maybeMoveB unit Backward)-- <*> destinationOfMoveB (maybeMoveB unit Forward)-- | Non empty region of the whole textunit where the current point is.regionOfNonEmptyB::TextUnit->BufferMRegionregionOfNonEmptyBunit=savingPointB$mkRegion<$>(maybeMoveBunitBackward>>pointB)<*>(moveBunitForward>>pointB)-- | Region between the point and the next boundary.-- The region is empty if the point is at the boundary.regionOfPartB::TextUnit->Direction->BufferMRegionregionOfPartBunitdir=mkRegion<$>pointB<*>destinationOfMoveB(maybeMoveBunitdir)-- | Non empty region between the point and the next boundary,-- In fact the region can be empty if we are at the end of file.regionOfPartNonEmptyB::TextUnit->Direction->BufferMRegionregionOfPartNonEmptyBunitdir=mkRegion<$>pointB<*>destinationOfMoveB(moveBunitdir)-- | Non empty region at given point and the next boundary,regionOfPartNonEmptyAtB::TextUnit->Direction->Point->BufferMRegionregionOfPartNonEmptyAtBunitdirp=dooldP<-pointBmoveTopr<-regionOfPartNonEmptyBunitdirmoveTooldPreturnrreadPrevUnitB::TextUnit->BufferMStringreadPrevUnitBunit=readRegionB=<<regionOfPartNonEmptyBunitBackwardreadUnitB::TextUnit->BufferMStringreadUnitB=readRegionB<=<regionOfBhalfUnit::Direction->TextUnit->TextUnithalfUnitdir(GenUnitenclosingboundary)=GenUnitenclosing(\d->ifd==dirthenboundarydelsereturnFalse)halfUnit_dirtu=tudeleteUnitB::TextUnit->Direction->BufferM()deleteUnitBunitdir=deleteRegionB=<<regionOfPartNonEmptyBunitdir