569 lines
15 KiB
Plaintext
569 lines
15 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
|
|
oTextCursor = getCursor().getText.createTextCursorByRange(getCursor())
|
|
' oTextCursor.gotoRange(oTextCursor.getStart(), False)
|
|
|
|
getTextCursor = oTextCursor
|
|
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())
|
|
End Sub
|
|
|
|
Sub setMode(modeName)
|
|
MODE = modeName
|
|
setRawStatus(modeName)
|
|
End Sub
|
|
|
|
Function gotoMode(sMode)
|
|
Select Case sMode
|
|
Case "NORMAL":
|
|
setMode("NORMAL")
|
|
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
|
|
|
|
|
|
' -----------------------------------
|
|
' 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
|
|
|
|
' --------------------
|
|
' 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
|
|
' Exit if plugin is not enabled
|
|
If IsMissing(VIBREOFFICE_ENABLED) Or Not VIBREOFFICE_ENABLED Then
|
|
KeyHandler_KeyPressed = False
|
|
Exit Function
|
|
End If
|
|
|
|
dim bConsumeInput, bIsMultiplier, bIsModified, bIsSpecial, oTextCursor
|
|
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)
|
|
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 ProcessModeKey(oEvent) Then
|
|
' Pass
|
|
|
|
' Multiplier Key
|
|
ElseIf ProcessNumberKey(oEvent) Then
|
|
bIsMultiplier = True
|
|
delaySpecialReset()
|
|
|
|
' Normal Key
|
|
ElseIf ProcessNormalKey(oEvent) Then
|
|
' Pass
|
|
|
|
' If is modified but doesn't match a normal command, allow input
|
|
' (Useful for built-in shortcuts like Ctrl+s, Ctrl+w)
|
|
ElseIf bIsModified Then
|
|
bConsumeInput = False
|
|
|
|
' If bIsSpecial but nothing matched, return to normal mode
|
|
ElseIf bIsSpecial Then
|
|
gotoMode("NORMAL")
|
|
End If
|
|
' --------------------------
|
|
|
|
' Reset Special
|
|
resetSpecial()
|
|
|
|
' Reset multiplier if last input was not number and not in special mode
|
|
If not bIsMultiplier and getSpecial() = "" Then
|
|
resetMultiplier()
|
|
End If
|
|
setStatus(getMultiplier())
|
|
|
|
' Show terminal-like cursor
|
|
oTextCursor = getTextCursor()
|
|
If MODE = "NORMAL" Then
|
|
cursorReset(oTextCursor)
|
|
ElseIf MODE = "INSERT" Then
|
|
oTextCursor.gotoRange(oTextCursor.getStart(), False)
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
End If
|
|
|
|
KeyHandler_KeyPressed = bConsumeInput
|
|
End Function
|
|
|
|
Function KeyHandler_KeyReleased(oEvent) As boolean
|
|
KeyHandler_KeyReleased = (MODE = "NORMAL") 'cancel KeyReleased
|
|
End Function
|
|
|
|
|
|
' ----------------
|
|
' Processing Keys
|
|
' ----------------
|
|
Function ProcessGlobalKey(oEvent)
|
|
dim bMatched
|
|
bMatched = True
|
|
Select Case oEvent.KeyCode
|
|
' PRESSED ESCAPE
|
|
Case 1281:
|
|
' 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")
|
|
Case Else:
|
|
bMatched = False
|
|
End Select
|
|
ProcessGlobalKey = 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 bMatched
|
|
bMatched = True
|
|
Select Case oEvent.KeyChar
|
|
' Insert modes
|
|
Case "i", "a", "I", "A":
|
|
If oEvent.KeyChar = "a" Then getCursor().goRight(1, False)
|
|
If oEvent.KeyChar = "I" Then ProcessMovementKey("^")
|
|
If oEvent.KeyChar = "A" Then ProcessMovementKey("$")
|
|
|
|
gotoMode("INSERT")
|
|
Case "v":
|
|
gotoMode("VISUAL")
|
|
Case Else:
|
|
bMatched = False
|
|
End Select
|
|
ProcessModeKey = bMatched
|
|
End Function
|
|
|
|
|
|
Function ProcessNormalKey(oEvent)
|
|
dim i, bMatched, bIsVisual, iIterations
|
|
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(oEvent.KeyChar, bIsVisual, oEvent.Modifiers)
|
|
bMatched = bMatched or bMatchedMovement
|
|
Next i
|
|
|
|
If bMatched Then
|
|
' If Special: d/c + movement
|
|
If bMatched And (getSpecial() = "d" Or getSpecial() = "c") Then
|
|
getTextCursor().setString("")
|
|
|
|
If getSpecial() = "d" Then gotoMode("NORMAL")
|
|
If getSpecial() = "c" Then gotoMode("INSERT")
|
|
|
|
End If
|
|
|
|
ProcessNormalKey = True
|
|
Exit Function
|
|
End If
|
|
|
|
' --------------------
|
|
' 2. Check Delete Key
|
|
' --------------------
|
|
bMatched = False
|
|
|
|
' Only 'x' can be done more than once
|
|
If oEvent.KeyChar <> "x" Then iIterations = 1
|
|
For i = 1 To iIterations
|
|
dim bMatchedDelete
|
|
|
|
' Delete Key
|
|
bMatchedDelete = ProcessDeleteKey(oEvent.KeyChar)
|
|
|
|
' Selection Modifier Key ??
|
|
|
|
bMatched = bMatched or bMatchedDelete
|
|
Next i
|
|
|
|
ProcessNormalKey = bMatched
|
|
End Function
|
|
|
|
|
|
Function ProcessDeleteKey(keyChar)
|
|
dim oTextCursor, bMatched
|
|
bMatched = True
|
|
|
|
Select Case keyChar
|
|
Case "x":
|
|
oTextCursor = getTextCursor()
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
oTextCursor.setString("")
|
|
|
|
' Reset Cursor
|
|
cursorReset(oTextCursor)
|
|
|
|
' Goto NORMAL mode (in the case of VISUAL mode)
|
|
gotoMode("NORMAL")
|
|
|
|
Case "D", "C":
|
|
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
|
|
|
|
getTextCursor().setString("")
|
|
|
|
If keyChar = "D" Then
|
|
gotoMode("NORMAL")
|
|
ElseIf keyChar = "C" Then
|
|
gotoMode("INSERT")
|
|
End IF
|
|
|
|
Case "d", "c":
|
|
If MODE = "VISUAL" Then
|
|
oTextCursor = getTextCursor()
|
|
thisComponent.getCurrentController.Select(oTextCursor)
|
|
oTextCursor.setString("")
|
|
|
|
If keyChar = "c" Then gotoMode("INSERT")
|
|
If keyChar = "d" Then gotoMode("NORMAL")
|
|
|
|
ElseIf MODE = "NORMAL" Then
|
|
setSpecial(keyChar)
|
|
gotoMode("VISUAL")
|
|
End If
|
|
|
|
|
|
Case Else:
|
|
bMatched = False
|
|
End Select
|
|
|
|
ProcessDeleteKey = 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
|
|
|
|
Select Case keyChar
|
|
Case "l":
|
|
oTextCursor.goRight(1, bExpand)
|
|
Case "h":
|
|
oTextCursor.goLeft(1, bExpand)
|
|
|
|
' oTextCursor.goUp and oTextCursor.goDown SHOULD work, but doesn't (I dont know why).
|
|
' So this is a weird hack
|
|
Case "k":
|
|
'oTextCursor.goUp(1, False)
|
|
getCursor().goUp(1, bExpand)
|
|
bSetCursor = False
|
|
Case "j":
|
|
'oTextCursor.goDown(1, False)
|
|
getCursor().goDown(1, bExpand)
|
|
bSetCursor = False
|
|
' ----------
|
|
|
|
Case "^":
|
|
getCursor().gotoStartOfLine(bExpand)
|
|
bSetCursor = False
|
|
Case "$":
|
|
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
|
|
|
|
Case "w", "W":
|
|
oTextCursor.gotoNextWord(bExpand)
|
|
Case "b", "B":
|
|
oTextCursor.gotoPreviousWord(bExpand)
|
|
Case "e":
|
|
oTextCursor.gotoEndOfWord(bExpand)
|
|
|
|
Case ")":
|
|
oTextCursor.gotoNextSentence(bExpand)
|
|
Case "(":
|
|
oTextCursor.gotoPreviousSentence(bExpand)
|
|
Case "}":
|
|
oTextCursor.gotoNextParagraph(bExpand)
|
|
Case "{":
|
|
oTextCursor.gotoPreviousParagraph(bExpand)
|
|
Case Else:
|
|
bSetCursor = False
|
|
bMatched = False
|
|
End Select
|
|
|
|
' If oTextCursor was moved, set global cursor to its position
|
|
If bSetCursor Then
|
|
getCursor().gotoRange(oTextCursor.getStart(), False)
|
|
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()
|
|
cursorReset(oTextCursor)
|
|
|
|
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
|