diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c62ec65
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,22 @@
+run_testing: testing
+ killall soffice.bin || echo "No libreoffice instance found"
+ lowriter "$$TESTING_ODT" --norestore &
+
+testing: src/vibreoffice.vbs
+ ./compile.sh "src/vibreoffice.vbs" "$$TESTING_XBA"
+
+extension: clean src/vibreoffice.vbs
+ if [ -z "$$VIBREOFFICE_VERSION" ]; then \
+ echo "VIBREOFFICE_VERSION must be set"; \
+ else \
+ mkdir -p build; mkdir -p dist; \
+ cp -r extension/template build/template; \
+ ./compile.sh "src/vibreoffice.vbs" "build/template/vibreoffice/vibreoffice.xba"; \
+ cd "build/template"; \
+ sed -i "s/%VIBREOFFICE_VERSION%/$$VIBREOFFICE_VERSION/g" description.xml; \
+ zip -r "../../dist/vibreoffice-$$VIBREOFFICE_VERSION.oxt" .; \
+ fi
+
+.PHONY: clean
+clean:
+ rm -rf build
diff --git a/README.md b/README.md
index 174639d..acda449 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,30 @@
# vibreoffice
-A Vi/Vim Mode Extension for Libreoffice and OpenOffice (Apache OpenOffice, OpenOffice.org)
-### Installation
-Coming soon.
+vibreoffice is an extension for Libreoffice and OpenOffice that brings some of
+your favorite key bindings from vi/vim to your favorite office suite. It is
+obviously not meant to be feature-complete, but hopefully will be useful to
+both vi/vim neophytes and experts alike.
+
+### Installation/Usage
+
+The easiest way to install is to download the
+[latest extension file](https://raw.github.com/seanyeh/vibreoffice/master/dist/vibreoffice-0.1.0.oxt)
+and open it with LibreOffice/OpenOffice.
+
+To enable/disable vibreoffice, simply select Tools -> Add-Ons -> vibreoffice.
+
+If you really want to, you can build the .oxt file yourself by running
+```shell
+# replace 0.0.0 with your desired version number
+VIBREOFFICE_VERSION="0.0.0" make extension
+```
+This will simply build the extension file from the template files in
+`extension/template`. These template files were auto-generated using
+[Extension Compiler](https://wiki.openoffice.org/wiki/Extensions_Packager#Download).
+
### Features
+
vibreoffice currently supports:
- Insert (`i`, `I`, `a`, `A`, `o`, `O`), Visual (`v`), Normal modes
- Movement keys: `hjkl`, `w`, `W`, `b`, `B`, `e`, `$`, `^`, `{}`, `()`, `C-d`, `C-u`
@@ -14,7 +34,35 @@ vibreoffice currently supports:
- Deletion: `x`, `d`, `c`, `s`, `D`, `C`, `S`, `dd`, `cc`
- Plus movement and number modifiers: e.g. `5dw`, `c3j`, `2dfe`
- Delete a/inner block: e.g. `di(`, `da{`, `ci[`, `ci"`, `ca'`, `dit`
-- More to come!
+
+### Known differences/issues
+
+If you are familiar with vi/vim, then vibreoffice should give very few
+surprises. However, there are some differences, primarily due to word
+processor-text editor differences or limitations of the LibreOffice API and/or
+my patience.
+- The concept of lines in a text editor is not quite analogous to that of a
+ word processor. I made my best effort to incorporate the line analogy while keeping
+ the spirit of word processing.
+ - Unlike vi/vim, movement keys will wrap to the next line
+ - Due to line wrapping, you may find your cursor move up/down a line for
+ commands that would otherwise leave you in the same position (such as `dd`)
+- Currently, I am using LibreOffice's built-in word detection for word
+ movements (`w`, `W`, etc.) which differs slightly from vi's. For many
+ situations I find this satisfactory, but there are some funky cases involving
+ certain symbols. I may or may not change this in the future.
+- vibreoffice does not have contextual awareness. What I mean by that is that
+ it does not keep track of which parentheses/braces match. Hence, you may have
+ unexpected behavior (using commands such as `di(`) if your document has
+ syntatically uneven parentheses/braces or nesting of such symbols. I don't
+ intend to fix this for now, as I don't believe this is a critical feature for
+ word processing.
+- Using `d`, `c` (or any of their variants) will temporarily bring you into
+ Visual mode. This is intentional and should not have any noticeable effects.
+
+vibreoffice is new, so it is bound to have plenty of bugs. Please let me know
+if you run into anything!
+
### License
vibreoffice is released under the MIT License.
diff --git a/compile.sh b/compile.sh
new file mode 100755
index 0000000..ba6aa03
--- /dev/null
+++ b/compile.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+#
+# "Compile" BASIC macro file to LibreOffice-compatible xba file
+
+# compile SRC DESTINATION
+compile() {
+ # Escape XML &<>'"
+ src=`sed "s/\&/\&/g; s/\</g; s/>/\>/g; s/'/\'/g; s/\"/\"/g" "$1"`
+
+ xbafile="$2"
+ name="`basename -s \".xba\" "$xbafile"`"
+
+ XBA_TEMPLATE='\n\n\n%s\n'
+
+ printf "$XBA_TEMPLATE" "$name" "$src" > "$xbafile"
+}
+
+if [ "$#" -ne 2 ]; then
+ echo "Usage: $0 SOURCE DESTINATION"
+else
+ compile "$1" "$2"
+fi
diff --git a/dist/vibreoffice-0.1.0.oxt b/dist/vibreoffice-0.1.0.oxt
new file mode 100644
index 0000000..d4542b1
Binary files /dev/null and b/dist/vibreoffice-0.1.0.oxt differ
diff --git a/extension/template/AddonUI.xcu b/extension/template/AddonUI.xcu
new file mode 100644
index 0000000..36155a0
--- /dev/null
+++ b/extension/template/AddonUI.xcu
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ com.sun.star.text.TextDocument,com.sun.star.drawing.DrawingDocument,com.sun.star.presentation.PresentationDocument
+
+
+ vibreoffice - Toggle Vi Mode
+
+
+ vnd.sun.star.script:vibreoffice.vibreoffice.Main?language=Basic&location=application
+
+
+ _self
+
+
+
+
+
+
diff --git a/extension/template/Descriptions/descr-en.txt b/extension/template/Descriptions/descr-en.txt
new file mode 100644
index 0000000..5496090
--- /dev/null
+++ b/extension/template/Descriptions/descr-en.txt
@@ -0,0 +1,2 @@
+Vi Mode Extension for LibreOffice/OpenOffice
+
diff --git a/extension/template/META-INF/manifest.xml b/extension/template/META-INF/manifest.xml
new file mode 100644
index 0000000..7b1bdc4
--- /dev/null
+++ b/extension/template/META-INF/manifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/extension/template/description.xml b/extension/template/description.xml
new file mode 100644
index 0000000..9354c0b
--- /dev/null
+++ b/extension/template/description.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Vibreoffice - Vi-Mode Extension
+
+
+ Sean Yeh
+
+
+
+
+
diff --git a/extension/template/help/en/vibreoffice/Pagexx.xhp b/extension/template/help/en/vibreoffice/Pagexx.xhp
new file mode 100644
index 0000000..f80e91c
--- /dev/null
+++ b/extension/template/help/en/vibreoffice/Pagexx.xhp
@@ -0,0 +1,35 @@
+
+
+
+
+ Vibreoffice - Vi-Mode Extension : Write the title here
+ /vibreoffice/Pagexx.xhp
+
+
+
+
+ Write your own help pages from here
+
+
+ Language en is the default value, you should have an english version of your help if your extension is internationally available.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extension/template/vibreoffice/dialog.xlb b/extension/template/vibreoffice/dialog.xlb
new file mode 100644
index 0000000..806f969
--- /dev/null
+++ b/extension/template/vibreoffice/dialog.xlb
@@ -0,0 +1,3 @@
+
+
+
diff --git a/extension/template/vibreoffice/script.xlb b/extension/template/vibreoffice/script.xlb
new file mode 100644
index 0000000..df7b26b
--- /dev/null
+++ b/extension/template/vibreoffice/script.xlb
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/extension/template/vibreoffice/vibreoffice.xba b/extension/template/vibreoffice/vibreoffice.xba
new file mode 100644
index 0000000..0f8ec68
--- /dev/null
+++ b/extension/template/vibreoffice/vibreoffice.xba
@@ -0,0 +1,422 @@
+
+
+
+REM ***** BASIC *****
+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
+
+global isRunning as boolean
+
+' -----------
+' 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)
+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
+
+
+' --------------------
+' 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)
+
+ 'oXKeyHandler = Nothing 'To know later this handler has stopped.
+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, 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)
+
+ ' MsgBox(oEvent.KeyCode & "," & oEvent.KeyChar & "," & oEvent.KeyFunc & "," & oEvent.Modifiers)
+
+ ' --------------------------
+ ' Process global shortcuts, exit if matched (like ESC)
+ If ProcessGlobalKey(oEvent) Then
+ ' Pass
+
+ ElseIf MODE = "INSERT" Then
+ bConsumeInput = False ' Allow all inputs
+
+ ' If Change Mode
+ ElseIf ProcessModeKey(oEvent) Then
+ REM do nothing
+
+ ElseIf ProcessNumberKey(oEvent) Then
+ bIsMultiplier = True
+
+ ' Normal Key
+ ElseIf Not ProcessNormalKey(oEvent) and bIsModified Then
+ ' If is modified but doesn't match a normal command, allow input
+ ' (Useful for built-in shortcuts like Ctrl+s, Ctrl+w)
+ bConsumeInput = False
+ End If
+ ' --------------------------
+
+
+ ' Reset multiplier
+ If not bIsMultiplier Then resetMultiplier()
+ setStatus(getMultiplier())
+
+ ' Show terminal-like cursor
+ oTextCursor = getTextCursor()
+ If MODE = "NORMAL" Then
+ oTextCursor.gotoRange(oTextCursor.getStart(), False)
+ oTextCursor.goRight(1, False)
+ oTextCursor.goLeft(1, True)
+ thisComponent.getCurrentController.Select(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
+
+ 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
+ bMatched = False
+ bIsVisual = (MODE = "VISUAL") ' is this hardcoding bad? what about visual block?
+ For i = 1 To getMultiplier()
+ dim bMatchedMovement, bMatchedDelete
+
+ bMatchedMovement = ProcessMovementKey(oEvent.KeyChar, bIsVisual, oEvent.Modifiers)
+ bMatchedDelete = ProcessDeleteKey(oEvent)
+ bMatched = bMatched or bMatchedMovement or bMatchedDelete
+
+ ' Special case: Break from For loop if in visual mode and has deleted,
+ ' since multiplier should not be applied
+ If bIsVisual and bMatchedDelete Then Exit For
+ Next i
+
+ ProcessNormalKey = bMatched
+End Function
+
+Function ProcessDeleteKey(oEvent)
+ dim oTextCursor, bMatched
+ oTextCursor = getTextCursor()
+ bMatched = True
+ Select Case oEvent.KeyChar
+ ' Case "d":
+ ' setSpecial("d")
+
+ Case "x":
+ ' oTextCursor.goRight(1, True)
+ thisComponent.getCurrentController.Select(oTextCursor)
+ '
+ oTextCursor.setString("")
+ 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 isControl
+ isControl = (keyModifiers = 2) or (keyModifiers = 8)
+
+ ' Ctrl+d and Ctrl+u
+ If isControl and keyChar = "d" Then
+ getCursor().ScreenDown(bExpand)
+ ElseIf isControl 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)
+
+ ' BEGIN HACK
+ ' 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
+ ' END HACK
+
+ 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
+ ' Initializing
+ VIEW_CURSOR = thisComponent.getCurrentController.getViewCursor
+ resetMultiplier()
+
+ gotoMode("NORMAL")
+
+ sStartXKeyHandler()
+End Sub
+
+Sub Main
+ If Not VIBREOFFICE_STARTED Then
+ initVibreoffice()
+ VIBREOFFICE_STARTED = True
+ End If
+
+ ' Toggle enable/disable
+ VIBREOFFICE_ENABLED = Not VIBREOFFICE_ENABLED
+End Sub
+
\ No newline at end of file