1038 lines
29 KiB
Plaintext
1038 lines
29 KiB
Plaintext
' vibreoffice - Vi Mode for LibreOffice/OpenOffice
|
|
'
|
|
' The MIT License (MIT)
|
|
'
|
|
' Copyright (c) 2014 Sean Yeh
|
|
'
|
|
' Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
' of this software and associated documentation files (the "Software"), to deal
|
|
' in the Software without restriction, including without limitation the rights
|
|
' to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
' copies of the Software, and to permit persons to whom the Software is
|
|
' furnished to do so, subject to the following conditions:
|
|
'
|
|
' The above copyright notice and this permission notice shall be included in
|
|
' all copies or substantial portions of the Software.
|
|
'
|
|
' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
' OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
' THE SOFTWARE.
|
|
|
|
Option Explicit
|
|
|
|
' --------
|
|
' Globals
|
|
' --------
|
|
global VIBREOFFICE_STARTED as boolean ' Defaults to False
|
|
global VIBREOFFICE_ENABLED as boolean ' Defaults to False
|
|
|
|
global oXKeyHandler as object
|
|
|
|
' Global State
|
|
global MODE as string
|
|
global VIEW_CURSOR as object
|
|
global MULTIPLIER as integer
|
|
|
|
' -----------
|
|
' Singletons
|
|
' -----------
|
|
Function getCursor
|
|
getCursor = VIEW_CURSOR
|
|
End Function
|
|
|
|
Function getTextCursor
|
|
dim oTextCursor
|
|
On Error Goto ErrorHandler
|
|
oTextCursor = getCursor().getText.createTextCursorByRange(getCursor())
|
|
|
|
getTextCursor = oTextCursor
|
|
Exit Function
|
|
|
|
ErrorHandler:
|
|
' Text Cursor does not work in some instances, such as in Annotations
|
|
getTextCursor = Nothing
|
|
End Function
|
|
|
|
' -----------------
|
|
' Helper Functions
|
|
' -----------------
|
|
Sub restoreStatus 'restore original statusbar
|
|
dim oLayout
|
|
oLayout = thisComponent.getCurrentController.getFrame.LayoutManager
|
|
oLayout.destroyElement("private:resource/statusbar/statusbar")
|
|
oLayout.createElement("private:resource/statusbar/statusbar")
|
|
End Sub
|
|
|
|
Sub setRawStatus(rawText)
|
|
thisComponent.Currentcontroller.StatusIndicator.Start(rawText, 0)
|
|
End Sub
|
|
|
|
Sub setStatus(statusText)
|
|
setRawStatus(MODE & " | " & statusText & " | special: " & getSpecial() & " | " & "modifier: " & getMovementModifier())
|
|
End Sub
|
|
|
|
Sub setMode(modeName)
|
|
MODE = modeName
|
|
setRawStatus(modeName)
|
|
End Sub
|
|
|
|
Function gotoMode(sMode)
|
|
Select Case sMode
|
|
Case "NORMAL":
|
|
setMode("NORMAL")
|
|
setMovementModifier("")
|
|
Case "INSERT":
|
|
setMode("INSERT")
|
|
Case "VISUAL":
|
|
setMode("VISUAL")
|
|
|
|
dim oTextCursor
|
|
oTextCursor = getTextCursor()
|
|
' Deselect TextCursor
|
|
oTextCursor.gotoRange(oTextCursor.getStart(), False)
|
|
' Show TextCursor selection
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
End Select
|
|
End Function
|
|
|
|
Sub cursorReset(oTextCursor)
|
|
oTextCursor.gotoRange(oTextCursor.getStart(), False)
|
|
oTextCursor.goRight(1, False)
|
|
oTextCursor.goLeft(1, True)
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
End Sub
|
|
|
|
Function samePos(oPos1, oPos2)
|
|
samePos = oPos1.X() = oPos2.X() And oPos1.Y() = oPos2.Y()
|
|
End Function
|
|
|
|
Function genString(sChar, iLen)
|
|
dim sResult, i
|
|
sResult = ""
|
|
For i = 1 To iLen
|
|
sResult = sResult & sChar
|
|
Next i
|
|
genString = sResult
|
|
End Function
|
|
|
|
' Yanks selection to system clipboard.
|
|
' If bDelete is true, will delete selection.
|
|
Sub yankSelection(bDelete)
|
|
dim dispatcher As Object
|
|
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
|
|
dispatcher.executeDispatch(ThisComponent.CurrentController.Frame, ".uno:Copy", "", 0, Array())
|
|
|
|
If bDelete Then
|
|
getTextCursor().setString("")
|
|
End If
|
|
End Sub
|
|
|
|
|
|
Sub pasteSelection()
|
|
dim oTextCursor, dispatcher As Object
|
|
|
|
' Deselect if in NORMAL mode to avoid overwriting the character underneath
|
|
' the cursor
|
|
If MODE = "NORMAL" Then
|
|
oTextCursor = getTextCursor()
|
|
oTextCursor.gotoRange(oTextCursor.getStart(), False)
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
End If
|
|
|
|
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
|
|
dispatcher.executeDispatch(ThisComponent.CurrentController.Frame, ".uno:Paste", "", 0, Array())
|
|
End Sub
|
|
|
|
|
|
' -----------------------------------
|
|
' Special Mode (for chained commands)
|
|
' -----------------------------------
|
|
global SPECIAL_MODE As string
|
|
global SPECIAL_COUNT As integer
|
|
|
|
Sub setSpecial(specialName)
|
|
SPECIAL_MODE = specialName
|
|
|
|
If specialName = "" Then
|
|
SPECIAL_COUNT = 0
|
|
Else
|
|
SPECIAL_COUNT = 2
|
|
End If
|
|
End Sub
|
|
|
|
Function getSpecial()
|
|
getSpecial = SPECIAL_MODE
|
|
End Function
|
|
|
|
Sub delaySpecialReset()
|
|
SPECIAL_COUNT = SPECIAL_COUNT + 1
|
|
End Sub
|
|
|
|
Sub resetSpecial(Optional bForce)
|
|
If IsMissing(bForce) Then bForce = False
|
|
|
|
SPECIAL_COUNT = SPECIAL_COUNT - 1
|
|
If SPECIAL_COUNT <= 0 Or bForce Then
|
|
setSpecial("")
|
|
End If
|
|
End Sub
|
|
|
|
|
|
' -----------------
|
|
' Movement Modifier
|
|
' -----------------
|
|
'f,i,a
|
|
global MOVEMENT_MODIFIER As string
|
|
|
|
Sub setMovementModifier(modifierName)
|
|
MOVEMENT_MODIFIER = modifierName
|
|
End Sub
|
|
|
|
Function getMovementModifier()
|
|
getMovementModifier = MOVEMENT_MODIFIER
|
|
End Function
|
|
|
|
|
|
' --------------------
|
|
' Multiplier functions
|
|
' --------------------
|
|
Sub _setMultiplier(n as integer)
|
|
MULTIPLIER = n
|
|
End Sub
|
|
|
|
Sub resetMultiplier()
|
|
_setMultiplier(0)
|
|
End Sub
|
|
|
|
Sub addToMultiplier(n as integer)
|
|
dim sMultiplierStr as String
|
|
dim iMultiplierInt as integer
|
|
|
|
' Max multiplier: 10000 (stop accepting additions after 1000)
|
|
If MULTIPLIER <= 1000 then
|
|
sMultiplierStr = CStr(MULTIPLIER) & CStr(n)
|
|
_setMultiplier(CInt(sMultiplierStr))
|
|
End If
|
|
End Sub
|
|
|
|
' Should only be used if you need the raw value
|
|
Function getRawMultiplier()
|
|
getRawMultiplier = MULTIPLIER
|
|
End Function
|
|
|
|
' Same as getRawMultiplier, but defaults to 1 if it is unset (0)
|
|
Function getMultiplier()
|
|
If MULTIPLIER = 0 Then
|
|
getMultiplier = 1
|
|
Else
|
|
getMultiplier = MULTIPLIER
|
|
End If
|
|
End Function
|
|
|
|
|
|
' -------------
|
|
' Key Handling
|
|
' -------------
|
|
Sub sStartXKeyHandler
|
|
sStopXKeyHandler()
|
|
|
|
oXKeyHandler = CreateUnoListener("KeyHandler_", "com.sun.star.awt.XKeyHandler")
|
|
thisComponent.CurrentController.AddKeyHandler(oXKeyHandler)
|
|
End Sub
|
|
|
|
Sub sStopXKeyHandler
|
|
thisComponent.CurrentController.removeKeyHandler(oXKeyHandler)
|
|
End Sub
|
|
|
|
Sub XKeyHandler_Disposing(oEvent)
|
|
End Sub
|
|
|
|
|
|
' --------------------
|
|
' Main Key Processing
|
|
' --------------------
|
|
function KeyHandler_KeyPressed(oEvent) as boolean
|
|
dim oTextCursor
|
|
|
|
' Exit if plugin is not enabled
|
|
If IsMissing(VIBREOFFICE_ENABLED) Or Not VIBREOFFICE_ENABLED Then
|
|
KeyHandler_KeyPressed = False
|
|
Exit Function
|
|
End If
|
|
|
|
' Exit if TextCursor does not work (as in Annotations)
|
|
oTextCursor = getTextCursor()
|
|
If oTextCursor Is Nothing Then
|
|
KeyHandler_KeyPressed = False
|
|
Exit Function
|
|
End If
|
|
|
|
dim bConsumeInput, bIsMultiplier, bIsModified, bIsControl, bIsSpecial
|
|
bConsumeInput = True ' Block all inputs by default
|
|
bIsMultiplier = False ' reset multiplier by default
|
|
bIsModified = oEvent.Modifiers > 1 ' If Ctrl or Alt is held down. (Shift=1)
|
|
bIsControl = (oEvent.Modifiers = 2) or (oEvent.Modifiers = 8)
|
|
bIsSpecial = getSpecial() <> ""
|
|
|
|
|
|
' --------------------------
|
|
' Process global shortcuts, exit if matched (like ESC)
|
|
If ProcessGlobalKey(oEvent) Then
|
|
' Pass
|
|
|
|
' If INSERT mode, allow all inputs
|
|
ElseIf MODE = "INSERT" Then
|
|
bConsumeInput = False
|
|
|
|
' If Change Mode
|
|
' ElseIf MODE = "NORMAL" And Not bIsSpecial And getMovementModifier() = "" And ProcessModeKey(oEvent) Then
|
|
ElseIf ProcessModeKey(oEvent) Then
|
|
' Pass
|
|
|
|
' Replace Key
|
|
ElseIf getSpecial() = "r" And Not bIsModified Then
|
|
dim iLen
|
|
iLen = Len(getCursor().getString())
|
|
getCursor().setString(genString(oEvent.KeyChar, iLen))
|
|
|
|
' Multiplier Key
|
|
ElseIf ProcessNumberKey(oEvent) Then
|
|
bIsMultiplier = True
|
|
delaySpecialReset()
|
|
|
|
' Normal Key
|
|
ElseIf ProcessNormalKey(oEvent.KeyChar, oEvent.Modifiers) Then
|
|
' Pass
|
|
|
|
' If is modified but doesn't match a normal command, allow input
|
|
' (Useful for built-in shortcuts like Ctrl+a, Ctrl+s, Ctrl+w)
|
|
ElseIf bIsModified Then
|
|
' Ctrl+a (select all) sets mode to VISUAL
|
|
If bIsControl And oEvent.KeyChar = "a" Then
|
|
gotoMode("VISUAL")
|
|
End If
|
|
bConsumeInput = False
|
|
|
|
' Movement modifier here?
|
|
ElseIf ProcessMovementModifierKey(oEvent.KeyChar) Then
|
|
delaySpecialReset()
|
|
|
|
' If standard movement key (in VISUAL mode) like arrow keys, home, end
|
|
ElseIf MODE = "VISUAL" And ProcessStandardMovementKey(oEvent) Then
|
|
' Pass
|
|
|
|
' If bIsSpecial but nothing matched, return to normal mode
|
|
ElseIf bIsSpecial Then
|
|
gotoMode("NORMAL")
|
|
|
|
' Allow non-letter keys if unmatched
|
|
ElseIf asc(oEvent.KeyChar) = 0 Then
|
|
bConsumeInput = False
|
|
End If
|
|
' --------------------------
|
|
|
|
' Reset Special
|
|
resetSpecial()
|
|
|
|
' Reset multiplier if last input was not number and not in special mode
|
|
If not bIsMultiplier and getSpecial() = "" and getMovementModifier() = "" Then
|
|
resetMultiplier()
|
|
End If
|
|
setStatus(getMultiplier())
|
|
|
|
KeyHandler_KeyPressed = bConsumeInput
|
|
End Function
|
|
|
|
Function KeyHandler_KeyReleased(oEvent) As boolean
|
|
dim oTextCursor
|
|
|
|
' Show terminal-like cursor
|
|
oTextCursor = getTextCursor()
|
|
If oTextCursor Is Nothing Then
|
|
' Do nothing
|
|
ElseIf oEvent.Modifiers = 2 Or oEvent.Modifiers = 8 And oEvent.KeyChar = "c" Then
|
|
' Allow Ctrl+c for Copy, so don't change cursor
|
|
' Pass
|
|
ElseIf MODE = "NORMAL" Then
|
|
cursorReset(oTextCursor)
|
|
ElseIf MODE = "INSERT" Then
|
|
oTextCursor.gotoRange(oTextCursor.getStart(), False)
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
End If
|
|
|
|
KeyHandler_KeyReleased = (MODE = "NORMAL") 'cancel KeyReleased
|
|
End Function
|
|
|
|
|
|
' ----------------
|
|
' Processing Keys
|
|
' ----------------
|
|
Function ProcessGlobalKey(oEvent)
|
|
dim bMatched, bIsControl
|
|
bMatched = True
|
|
bIsControl = (oEvent.Modifiers = 2) or (oEvent.Modifiers = 8)
|
|
|
|
' PRESSED ESCAPE (or ctrl+[)
|
|
if oEvent.KeyCode = 1281 Or (oEvent.KeyCode = 1315 And bIsControl) Then
|
|
' Move cursor back if was in INSERT (but stay on same line)
|
|
If MODE <> "NORMAL" And Not getCursor().isAtStartOfLine() Then
|
|
getCursor().goLeft(1, False)
|
|
End If
|
|
|
|
resetSpecial(True)
|
|
gotoMode("NORMAL")
|
|
Else
|
|
bMatched = False
|
|
End If
|
|
ProcessGlobalKey = bMatched
|
|
End Function
|
|
|
|
|
|
Function ProcessStandardMovementKey(oEvent)
|
|
dim c, bMatched
|
|
c = oEvent.KeyCode
|
|
|
|
bMatched = True
|
|
|
|
If MODE <> "VISUAL" Then
|
|
bMatched = False
|
|
'Pass
|
|
ElseIf c = 1024 Then
|
|
ProcessMovementKey("j", True)
|
|
ElseIf c = 1025 Then
|
|
ProcessMovementKey("k", True)
|
|
ElseIf c = 1026 Then
|
|
ProcessMovementKey("h", True)
|
|
ElseIf c = 1027 Then
|
|
ProcessMovementKey("l", True)
|
|
ElseIf c = 1028 Then
|
|
ProcessMovementKey("^", True)
|
|
ElseIf c = 1029 Then
|
|
ProcessMovementKey("$", True)
|
|
Else
|
|
bMatched = False
|
|
End If
|
|
|
|
ProcessStandardMovementKey = bMatched
|
|
End Function
|
|
|
|
|
|
Function ProcessNumberKey(oEvent)
|
|
dim c
|
|
c = CStr(oEvent.KeyChar)
|
|
|
|
If c >= "0" and c <= "9" Then
|
|
addToMultiplier(CInt(c))
|
|
ProcessNumberKey = True
|
|
Else
|
|
ProcessNumberKey = False
|
|
End If
|
|
End Function
|
|
|
|
|
|
Function ProcessModeKey(oEvent)
|
|
dim bIsModified
|
|
bIsModified = oEvent.Modifiers > 1 ' If Ctrl or Alt is held down. (Shift=1)
|
|
' Don't change modes in these circumstances
|
|
If MODE <> "NORMAL" Or bIsModified Or getSpecial <> "" Or getMovementModifier() <> "" Then
|
|
ProcessModeKey = False
|
|
Exit Function
|
|
End If
|
|
|
|
' Mode matching
|
|
dim bMatched
|
|
bMatched = True
|
|
Select Case oEvent.KeyChar
|
|
' Insert modes
|
|
Case "i", "a", "I", "A", "o", "O":
|
|
If oEvent.KeyChar = "a" Then getCursor().goRight(1, False)
|
|
If oEvent.KeyChar = "I" Then ProcessMovementKey("^")
|
|
If oEvent.KeyChar = "A" Then ProcessMovementKey("$")
|
|
|
|
If oEvent.KeyChar = "o" Then
|
|
ProcessMovementKey("$")
|
|
ProcessMovementKey("l")
|
|
getCursor().setString(chr(13))
|
|
If Not getCursor().isAtStartOfLine() Then
|
|
getCursor().setString(chr(13) & chr(13))
|
|
ProcessMovementKey("l")
|
|
End If
|
|
End If
|
|
|
|
If oEvent.KeyChar = "O" Then
|
|
ProcessMovementKey("^")
|
|
getCursor().setString(chr(13))
|
|
If Not getCursor().isAtStartOfLine() Then
|
|
ProcessMovementKey("h")
|
|
getCursor().setString(chr(13))
|
|
ProcessMovementKey("l")
|
|
End If
|
|
End If
|
|
|
|
gotoMode("INSERT")
|
|
Case "v":
|
|
gotoMode("VISUAL")
|
|
Case Else:
|
|
bMatched = False
|
|
End Select
|
|
ProcessModeKey = bMatched
|
|
End Function
|
|
|
|
|
|
Function ProcessNormalKey(keyChar, modifiers)
|
|
dim i, bMatched, bIsVisual, iIterations, bIsControl
|
|
bIsControl = (modifiers = 2) or (modifiers = 8)
|
|
|
|
bIsVisual = (MODE = "VISUAL") ' is this hardcoding bad? what about visual block?
|
|
|
|
' ----------------------
|
|
' 1. Check Movement Key
|
|
' ----------------------
|
|
iIterations = getMultiplier()
|
|
bMatched = False
|
|
For i = 1 To iIterations
|
|
dim bMatchedMovement
|
|
|
|
' Movement Key
|
|
bMatchedMovement = ProcessMovementKey(KeyChar, bIsVisual, modifiers)
|
|
bMatched = bMatched or bMatchedMovement
|
|
|
|
|
|
' If Special: d/c + movement
|
|
If bMatched And (getSpecial() = "d" Or getSpecial() = "c" Or getSpecial() = "y") Then
|
|
yankSelection((getSpecial() <> "y"))
|
|
End If
|
|
Next i
|
|
|
|
' Reset Movement Modifier
|
|
setMovementModifier("")
|
|
|
|
' Exit already if movement key was matched
|
|
If bMatched Then
|
|
' If Special: d/c : change mode
|
|
If getSpecial() = "d" Or getSpecial() = "y" Then gotoMode("NORMAL")
|
|
If getSpecial() = "c" Then gotoMode("INSERT")
|
|
|
|
ProcessNormalKey = True
|
|
Exit Function
|
|
End If
|
|
|
|
|
|
' --------------------
|
|
' 2. Undo/Redo
|
|
' --------------------
|
|
If keyChar = "u" Or (bIsControl And keyChar = "r") Then
|
|
For i = 1 To iIterations
|
|
Undo(keyChar = "u")
|
|
Next i
|
|
|
|
ProcessNormalKey = True
|
|
Exit Function
|
|
End If
|
|
|
|
|
|
' --------------------
|
|
' 3. Paste
|
|
' Note: in vim, paste will result in cursor being over the last character
|
|
' of the pasted content. Here, the cursor will be the next character
|
|
' after that. Fix?
|
|
' --------------------
|
|
If keyChar = "p" or keyChar = "P" Then
|
|
' Move cursor right if "p" to paste after cursor
|
|
If keyChar = "p" Then
|
|
ProcessMovementKey("l", False)
|
|
End If
|
|
|
|
For i = 1 To iIterations
|
|
pasteSelection()
|
|
Next i
|
|
|
|
ProcessNormalKey = True
|
|
Exit Function
|
|
End If
|
|
|
|
|
|
' --------------------
|
|
' 4. Check Special/Delete Key
|
|
' --------------------
|
|
|
|
' There are no special/delete keys with modifier keys, so exit early
|
|
If modifiers > 1 Then
|
|
ProcessNormalKey = False
|
|
Exit Function
|
|
End If
|
|
|
|
' Only 'x' or Special (dd, cc) can be done more than once
|
|
If keyChar <> "x" and getSpecial() = "" Then
|
|
iIterations = 1
|
|
End If
|
|
For i = 1 To iIterations
|
|
dim bMatchedSpecial
|
|
|
|
' Special/Delete Key
|
|
bMatchedSpecial = ProcessSpecialKey(keyChar)
|
|
|
|
bMatched = bMatched or bMatchedSpecial
|
|
Next i
|
|
|
|
|
|
ProcessNormalKey = bMatched
|
|
End Function
|
|
|
|
|
|
' Function for both undo and redo
|
|
Sub Undo(bUndo)
|
|
On Error Goto ErrorHandler
|
|
|
|
If bUndo Then
|
|
thisComponent.getUndoManager().undo()
|
|
Else
|
|
thisComponent.getUndoManager().redo()
|
|
End If
|
|
Exit Sub
|
|
|
|
' Ignore errors from no more undos/redos in stack
|
|
ErrorHandler:
|
|
Resume Next
|
|
End Sub
|
|
|
|
|
|
Function ProcessSpecialKey(keyChar)
|
|
dim oTextCursor, bMatched, bIsSpecial, bIsDelete
|
|
bMatched = True
|
|
bIsSpecial = getSpecial() <> ""
|
|
|
|
|
|
If keyChar = "d" Or keyChar = "c" Or keyChar = "s" Or keyChar = "y" Then
|
|
bIsDelete = (keyChar <> "y")
|
|
|
|
' Special Cases: 'dd' and 'cc'
|
|
If bIsSpecial Then
|
|
dim bIsSpecialCase
|
|
bIsSpecialCase = (keyChar = "d" And getSpecial() = "d") Or (keyChar = "c" And getSpecial() = "c")
|
|
|
|
If bIsSpecialCase Then
|
|
ProcessMovementKey("^", False)
|
|
ProcessMovementKey("j", True)
|
|
|
|
oTextCursor = getTextCursor()
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
yankSelection(bIsDelete)
|
|
Else
|
|
bMatched = False
|
|
End If
|
|
|
|
' Go to INSERT mode after 'cc', otherwise NORMAL
|
|
If bIsSpecialCase And keyChar = "c" Then
|
|
gotoMode("INSERT")
|
|
Else
|
|
gotoMode("NORMAL")
|
|
End If
|
|
|
|
|
|
' visual mode: delete selection
|
|
ElseIf MODE = "VISUAL" Then
|
|
oTextCursor = getTextCursor()
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
|
|
yankSelection(bIsDelete)
|
|
|
|
If keyChar = "c" Or keyChar = "s" Then gotoMode("INSERT")
|
|
If keyChar = "d" Or keyChar = "y" Then gotoMode("NORMAL")
|
|
|
|
|
|
' Enter Special mode: 'd', 'c', or 'y' ('s' => 'cl')
|
|
ElseIf MODE = "NORMAL" Then
|
|
|
|
' 's' => 'cl'
|
|
If keyChar = "s" Then
|
|
setSpecial("c")
|
|
gotoMode("VISUAL")
|
|
ProcessNormalKey("l", 0)
|
|
Else
|
|
setSpecial(keyChar)
|
|
gotoMode("VISUAL")
|
|
End If
|
|
End If
|
|
|
|
' If is 'r' for replace
|
|
ElseIf keyChar = "r" Then
|
|
setSpecial("r")
|
|
|
|
' Otherwise, ignore if bIsSpecial
|
|
ElseIf bIsSpecial Then
|
|
bMatched = False
|
|
|
|
|
|
ElseIf keyChar = "x" Then
|
|
oTextCursor = getTextCursor()
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
yankSelection(True)
|
|
|
|
' Reset Cursor
|
|
cursorReset(oTextCursor)
|
|
|
|
' Goto NORMAL mode (in the case of VISUAL mode)
|
|
gotoMode("NORMAL")
|
|
|
|
ElseIf keyChar = "D" Or keyChar = "C" Then
|
|
If MODE = "VISUAL" Then
|
|
ProcessMovementKey("^", False)
|
|
ProcessMovementKey("$", True)
|
|
ProcessMovementKey("l", True)
|
|
Else
|
|
' Deselect
|
|
oTextCursor = getTextCursor()
|
|
oTextCursor.gotoRange(oTextCursor.getStart(), False)
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
ProcessMovementKey("$", True)
|
|
End If
|
|
|
|
yankSelection(True)
|
|
|
|
If keyChar = "D" Then
|
|
gotoMode("NORMAL")
|
|
ElseIf keyChar = "C" Then
|
|
gotoMode("INSERT")
|
|
End IF
|
|
|
|
' S only valid in NORMAL mode
|
|
ElseIf keyChar = "S" And MODE = "NORMAL" Then
|
|
ProcessMovementKey("^", False)
|
|
ProcessMovementKey("$", True)
|
|
yankSelection(True)
|
|
gotoMode("INSERT")
|
|
|
|
Else
|
|
bMatched = False
|
|
End If
|
|
|
|
ProcessSpecialKey = bMatched
|
|
End Function
|
|
|
|
|
|
Function ProcessMovementModifierKey(keyChar)
|
|
dim bMatched
|
|
|
|
bMatched = True
|
|
Select Case keyChar
|
|
Case "f", "t", "F", "T", "i", "a":
|
|
setMovementModifier(keyChar)
|
|
Case Else:
|
|
bMatched = False
|
|
End Select
|
|
|
|
ProcessMovementModifierKey = bMatched
|
|
End Function
|
|
|
|
|
|
Function ProcessSearchKey(oTextCursor, searchType, keyChar, bExpand)
|
|
'-----------
|
|
' Searching
|
|
'-----------
|
|
dim bMatched, oSearchDesc, oFoundRange, bIsBackwards, oStartRange
|
|
bMatched = True
|
|
bIsBackwards = (searchType = "F" Or searchType = "T")
|
|
|
|
If Not bIsBackwards Then
|
|
' VISUAL mode will goRight AFTER the selection
|
|
If MODE <> "VISUAL" Then
|
|
' Start searching from next character
|
|
oTextCursor.goRight(1, bExpand)
|
|
End If
|
|
|
|
oStartRange = oTextCursor.getEnd()
|
|
' Go back one
|
|
oTextCursor.goLeft(1, bExpand)
|
|
Else
|
|
oStartRange = oTextCursor.getStart()
|
|
End If
|
|
|
|
oSearchDesc = thisComponent.createSearchDescriptor()
|
|
oSearchDesc.setSearchString(keyChar)
|
|
oSearchDesc.SearchCaseSensitive = True
|
|
oSearchDesc.SearchBackwards = bIsBackwards
|
|
|
|
oFoundRange = thisComponent.findNext( oStartRange, oSearchDesc )
|
|
|
|
If not IsNull(oFoundRange) Then
|
|
dim oText, foundPos, curPos, bSearching
|
|
oText = oTextCursor.getText()
|
|
foundPos = oFoundRange.getStart()
|
|
|
|
' Unfortunately, we must go go to this "found" position one character at
|
|
' a time because I have yet to find a way to consistently move the
|
|
' Start range of the text cursor and leave the End range intact.
|
|
If bIsBackwards Then
|
|
curPos = oTextCursor.getEnd()
|
|
Else
|
|
curPos = oTextCursor.getStart()
|
|
End If
|
|
do until oText.compareRegionStarts(foundPos, curPos) = 0
|
|
If bIsBackwards Then
|
|
bSearching = oTextCursor.goLeft(1, bExpand)
|
|
curPos = oTextCursor.getStart()
|
|
Else
|
|
bSearching = oTextCursor.goRight(1, bExpand)
|
|
curPos = oTextCursor.getEnd()
|
|
End If
|
|
|
|
' Prevent infinite if unable to find, but shouldn't ever happen (?)
|
|
If Not bSearching Then
|
|
bMatched = False
|
|
Exit Do
|
|
End If
|
|
Loop
|
|
|
|
If searchType = "t" Then
|
|
oTextCursor.goLeft(1, bExpand)
|
|
ElseIf searchType = "T" Then
|
|
oTextCursor.goRight(1, bExpand)
|
|
End If
|
|
|
|
Else
|
|
bMatched = False
|
|
End If
|
|
|
|
' If matched, then we want to select PAST the character
|
|
' Else, this will counteract some weirdness. hack either way
|
|
If Not bIsBackwards And MODE = "VISUAL" Then
|
|
oTextCursor.goRight(1, bExpand)
|
|
End If
|
|
|
|
ProcessSearchKey = bMatched
|
|
|
|
End Function
|
|
|
|
|
|
Function ProcessInnerKey(oTextCursor, movementModifier, keyChar, bExpand)
|
|
dim bMatched, searchType1, searchType2, search1, search2
|
|
|
|
' Setting searchType
|
|
If movementModifier = "i" Then
|
|
searchType1 = "T" : searchType2 = "t"
|
|
ElseIf movementModifier = "a" Then
|
|
searchType1 = "F" : searchType2 = "f"
|
|
Else ' Shouldn't happen
|
|
ProcessInnerKey = False
|
|
Exit Function
|
|
End If
|
|
|
|
Select Case keyChar
|
|
Case "(", ")", "{", "}", "[", "]", "<", ">", "t", "'", """":
|
|
Select Case keyChar
|
|
Case "(", ")":
|
|
search1 = "(" : search2 = ")"
|
|
Case "{", "}":
|
|
search1 = "{" : search2 = "}"
|
|
Case "[", "]":
|
|
search1 = "[" : search2 = "}"
|
|
Case "<", ">":
|
|
search1 = "<" : search2 = ">"
|
|
Case "t":
|
|
search1 = ">" : search2 = "<"
|
|
Case "'":
|
|
search1 = "'" : search2 = "'"
|
|
Case """":
|
|
' Matches "smart" quotes, which is default in libreoffice
|
|
search1 = "“" : search2 = "”"
|
|
End Select
|
|
|
|
dim bMatched1, bMatched2
|
|
bMatched1 = ProcessSearchKey(oTextCursor, searchType1, search1, False)
|
|
bMatched2 = ProcessSearchKey(oTextCursor, searchType2, search2, True)
|
|
|
|
bMatched = (bMatched1 And bMatched2)
|
|
|
|
Case Else:
|
|
bMatched = False
|
|
|
|
End Select
|
|
|
|
ProcessInnerKey = bMatched
|
|
End Function
|
|
|
|
|
|
' -----------------------
|
|
' Main Movement Function
|
|
' -----------------------
|
|
' Default: bExpand = False, keyModifiers = 0
|
|
Function ProcessMovementKey(keyChar, Optional bExpand, Optional keyModifiers)
|
|
dim oTextCursor, bSetCursor, bMatched
|
|
oTextCursor = getTextCursor()
|
|
bMatched = True
|
|
If IsMissing(bExpand) Then bExpand = False
|
|
If IsMissing(keyModifiers) Then keyModifiers = 0
|
|
|
|
|
|
' Check for modified keys (Ctrl, Alt, not Shift)
|
|
If keyModifiers > 1 Then
|
|
dim bIsControl
|
|
bIsControl = (keyModifiers = 2) or (keyModifiers = 8)
|
|
|
|
' Ctrl+d and Ctrl+u
|
|
If bIsControl and keyChar = "d" Then
|
|
getCursor().ScreenDown(bExpand)
|
|
ElseIf bIsControl and keyChar = "u" Then
|
|
getCursor().ScreenUp(bExpand)
|
|
Else
|
|
bMatched = False
|
|
End If
|
|
|
|
ProcessMovementKey = bMatched
|
|
Exit Function
|
|
End If
|
|
|
|
' Set global cursor to oTextCursor's new position if moved
|
|
bSetCursor = True
|
|
|
|
|
|
' ------------------
|
|
' Movement matching
|
|
' ------------------
|
|
|
|
' ---------------------------------
|
|
' Special Case: Modified movements
|
|
If getMovementModifier() <> "" Then
|
|
Select Case getMovementModifier()
|
|
' f,F,t,T searching
|
|
Case "f", "t", "F", "T":
|
|
bMatched = ProcessSearchKey(oTextCursor, getMovementModifier(), keyChar, bExpand)
|
|
Case "i", "a":
|
|
bMatched = ProcessInnerKey(oTextCursor, getMovementModifier(), keyChar, bExpand)
|
|
|
|
Case Else:
|
|
bSetCursor = False
|
|
bMatched = False
|
|
End Select
|
|
|
|
If Not bMatched Then
|
|
bSetCursor = False
|
|
End If
|
|
' ---------------------------------
|
|
|
|
ElseIf keyChar = "l" Then
|
|
oTextCursor.goRight(1, bExpand)
|
|
|
|
ElseIf keyChar = "h" Then
|
|
oTextCursor.goLeft(1, bExpand)
|
|
|
|
' oTextCursor.goUp and oTextCursor.goDown SHOULD work, but doesn't (I dont know why).
|
|
' So this is a weird hack
|
|
ElseIf keyChar = "k" Then
|
|
'oTextCursor.goUp(1, False)
|
|
getCursor().goUp(1, bExpand)
|
|
bSetCursor = False
|
|
|
|
ElseIf keyChar = "j" Then
|
|
'oTextCursor.goDown(1, False)
|
|
getCursor().goDown(1, bExpand)
|
|
bSetCursor = False
|
|
' ----------
|
|
|
|
ElseIf keyChar = "^" Then
|
|
getCursor().gotoStartOfLine(bExpand)
|
|
bSetCursor = False
|
|
ElseIf keyChar = "$" Then
|
|
dim oldPos, newPos
|
|
oldPos = getCursor().getPosition()
|
|
getCursor().gotoEndOfLine(bExpand)
|
|
newPos = getCursor().getPosition()
|
|
|
|
' If the result is at the start of the line, then it must have
|
|
' jumped down a line; goLeft to return to the previous line.
|
|
' Except for: Empty lines (check for oldPos = newPos)
|
|
If getCursor().isAtStartOfLine() And oldPos.Y() <> newPos.Y() Then
|
|
getCursor().goLeft(1, bExpand)
|
|
End If
|
|
|
|
' maybe eventually cursorGoto... should return True/False for bsetCursor
|
|
bSetCursor = False
|
|
|
|
ElseIf keyChar = "w" or keyChar = "W" Then
|
|
oTextCursor.gotoNextWord(bExpand)
|
|
ElseIf keyChar = "b" or keyChar = "B" Then
|
|
oTextCursor.gotoPreviousWord(bExpand)
|
|
ElseIf keyChar = "e" Then
|
|
oTextCursor.gotoEndOfWord(bExpand)
|
|
|
|
ElseIf keyChar = ")" Then
|
|
oTextCursor.gotoNextSentence(bExpand)
|
|
ElseIf keyChar = "(" Then
|
|
oTextCursor.gotoPreviousSentence(bExpand)
|
|
ElseIf keyChar = "}" Then
|
|
oTextCursor.gotoNextParagraph(bExpand)
|
|
ElseIf keyChar = "{" Then
|
|
oTextCursor.gotoPreviousParagraph(bExpand)
|
|
|
|
Else
|
|
bSetCursor = False
|
|
bMatched = False
|
|
End If
|
|
|
|
' If oTextCursor was moved, set global cursor to its position
|
|
If bSetCursor Then
|
|
getCursor().gotoRange(oTextCursor.getStart(), False)
|
|
|
|
' ---- REALLY BAD HACK
|
|
' I can't seem to get the View Cursor (getCursor()) to update its
|
|
' position without calling its own movement functions.
|
|
' Theoretically, the above call to gotoRange should work, but I don't
|
|
' know why it doesn't. Visually it works, but its X position is reset
|
|
' when you move lines. Bug??
|
|
dim oTempPos
|
|
oTempPos = getCursor().getPosition()
|
|
' Move left 1 and then right 1 to stay in same position
|
|
getCursor().goLeft(1, False)
|
|
If Not samePos(oTempPos, getCursor().getPosition()) Then
|
|
getCursor().goRight(1, False)
|
|
End If
|
|
End If
|
|
|
|
' If oTextCursor was moved and is in VISUAL mode, update selection
|
|
if bSetCursor and bExpand then
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
end if
|
|
|
|
ProcessMovementKey = bMatched
|
|
End Function
|
|
|
|
|
|
Sub initVibreoffice
|
|
dim oTextCursor
|
|
' Initializing
|
|
VIBREOFFICE_STARTED = True
|
|
VIEW_CURSOR = thisComponent.getCurrentController.getViewCursor
|
|
|
|
resetMultiplier()
|
|
gotoMode("NORMAL")
|
|
|
|
' Show terminal cursor
|
|
oTextCursor = getTextCursor()
|
|
|
|
If oTextCursor Is Nothing Then
|
|
' Do nothing
|
|
Else
|
|
cursorReset(oTextCursor)
|
|
End If
|
|
|
|
sStartXKeyHandler()
|
|
End Sub
|
|
|
|
|
|
Sub Main
|
|
If Not VIBREOFFICE_STARTED Then
|
|
initVibreoffice()
|
|
End If
|
|
|
|
' Toggle enable/disable
|
|
VIBREOFFICE_ENABLED = Not VIBREOFFICE_ENABLED
|
|
|
|
' Restore statusbar
|
|
If Not VIBREOFFICE_ENABLED Then restoreStatus()
|
|
End Sub
|