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" "$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