Skip to content

Commit

Permalink
Implemented auto-typing into starbase
Browse files Browse the repository at this point in the history
  • Loading branch information
dbaumgarten committed Jan 4, 2021
1 parent dbb6653 commit d83340b
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 11 deletions.
4 changes: 3 additions & 1 deletion cmd/langserv.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
)

var logfile string
var hotkeys bool

// langservCmd represents the langserv command
var langservCmd = &cobra.Command{
Expand All @@ -19,7 +20,7 @@ var langservCmd = &cobra.Command{
stream := langserver.NewStdioStream()
configureFileLogging()
stream.Log = debugLog
err := langserver.Run(context.Background(), stream)
err := langserver.Run(context.Background(), stream, hotkeys)
if err != nil {
log.Println(err)
}
Expand All @@ -30,4 +31,5 @@ func init() {
rootCmd.AddCommand(langservCmd)
langservCmd.Flags().StringVar(&logfile, "logfile", "", "Name of the file to log into. Defaults to stderr")
langservCmd.Flags().BoolVarP(&debugLog, "debug", "d", false, "Enable verbose debug-logging")
langservCmd.Flags().BoolVar(&hotkeys, "hotkeys", true, "Enable system-wide hotkeys for auto-typing")
}
20 changes: 19 additions & 1 deletion docs/vscode-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ There are several commands that can be executed from the command-palette (f1). Y
- **Run all \*_test.yaml**: Will run the testcases in all *_test.yaml files in the current directory and report the results.
- **Compile NOLOL**: If you have a [.nolol-file](/nolol) open, run this action to compile it to yolol. This will generate a file \<name\>.yolol in the same directory, containing the compiled code.

# Auto-typing into Starbase
One key-problem when writing yolol-code in an external editor always was how to get the code into your ship's yolol-chips. The easiest way was to copy/paste it line by line. Vscode-yolol can make this much faster, but offering the ability to auto-type your scripts directly into starbase.

***NOTE***: Please keep the Ctrl-Key pressed the whole time the auto-typer is doing work. Otherwise the key-up-event produced by you letting up the key can interfere with the auto-typing.

***NOTE2***: Make sure to you really have focussed the chip when using the hotkey, otherwise vscode-yolol will send random keystrokes to the game, making your character do all sorts of weird stuff.

If you (for whatever reason) want to disable the global hotkeys, you can do so in the vscode-settings under File->Preferences->Settings->search for 'yolol'->Hotkeys: Enable). Vscode needs to be restarted for changes to this setting to take effect.

## Inseting code into a chip
Open the .yolol-script you want to insert in vscode (it has to be the current active file). Go to Stabase's window and open the yolol-chip you want to fill. Unlock it, aim your cursor at it and click a line. Now press ```Ctrl+I```. Vscode will start to auto-type your code into the chip, starting at your current cursor-position.


## Erasing a chip
The auto-typing only works properly when the lines of the chip are empty before inserting code. Vscode-yolol can also automate this for you. Click a line on your chip and press ```Ctrl+D```. This will end the key-strokes Ctrl+A, Entf, Down 20 times, resulting in an empty chip, starting at the line you clicked. (If you clicked line 1, the chip is now completely empty)

## Overwriting code
By pressing ```Ctrl+O``` vscode will overwrite the code on the chip, starting from the current, line with the yolol-code of the currently open script in vscode. (This is effectively a faster variant of ```Ctrl+D```+Select line+```Ctrl+I```)

# Debugging
This extension enables you to interactively run and debug yolol-code. To learn how to debug using vscode see here: https://code.visualstudio.com/Docs/editor/debugging .
Expand Down Expand Up @@ -85,4 +103,4 @@ If you continued execution of a script, but nothing seems to happen, make sure a

## Runtime-errors

If your script encounters a runtime-error, the debugger will automatically pause. However, some scripts use runtime-errors for regular control-flow. You can disable the auto-pausing by setting "ignoreErrs": true in the launch-configuration.
If your script encounters a runtime-error, the debugger will automatically pause. However, some scripts use runtime-errors for regular control-flow. You can disable the auto-pausing by setting "ignoreErrs": true in the launch-configuration.
8 changes: 8 additions & 0 deletions pkg/langserver/autotype_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// +build !windows

package langserver

// ListenForHotkeys is only supported on windows
func (ls *LangServer) ListenForHotkeys() {

}
110 changes: 110 additions & 0 deletions pkg/langserver/autotype_win.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// +build windows

package langserver

import (
"fmt"
"os"
"strings"
"time"

"github.com/dbaumgarten/yodk/pkg/langserver/win32"
)

var (
// AutotypeHotkey is the hotkey to trigger auto-typing
AutotypeHotkey = &win32.Hotkey{
ID: 1,
Modifiers: win32.ModCtrl,
KeyCode: 'I',
}
// AutodeleteHotkey is the hotkey to trigger auto-deletion
AutodeleteHotkey = &win32.Hotkey{
ID: 2,
Modifiers: win32.ModCtrl,
KeyCode: 'D',
}
// AutooverwriteHotkey is the hotkey to overwrite the current line with new ones
AutooverwriteHotkey = &win32.Hotkey{
ID: 3,
Modifiers: win32.ModCtrl,
KeyCode: 'O',
}
)

const typeDelay = 40 * time.Millisecond

// ListenForHotkeys listens for global hotkeys and dispatches the registered actions
func (ls *LangServer) ListenForHotkeys() {
go func() {
err := win32.ListenForHotkeys(nil, ls.hotkeyHandler, AutotypeHotkey, AutodeleteHotkey, AutooverwriteHotkey)
if err != nil {
fmt.Fprintf(os.Stderr, "Error when registering hotkeys: %s", err)
}
}()
}

func (ls *LangServer) hotkeyHandler(hk win32.Hotkey) {
win32.SendInput(win32.KeyUpInput(win32.KeycodeCtrl))
win32.SendInput(win32.KeyUpInput(uint16(hk.KeyCode)))
switch hk.ID {
case AutotypeHotkey.ID:
if code := ls.getLastOpenedCode(); code == code {
typeYololCode(code)
}
case AutodeleteHotkey.ID:
deleteAllLines()
case AutooverwriteHotkey.ID:
if code := ls.getLastOpenedCode(); code == code {
overwriteYololCode(code)
}
}
}

func (ls *LangServer) getLastOpenedCode() string {
ls.cache.Lock.Lock()
lastOpened := ls.cache.LastOpenedYololFile
ls.cache.Lock.Unlock()

if lastOpened != "" {
code, err := ls.cache.Get(lastOpened)
if err == nil {
return code
}
}
return ""
}

func typeYololCode(code string) {
lines := strings.Split(code, "\n")
for _, line := range lines {
win32.SendString(line)
time.Sleep(typeDelay)
win32.SendInput(win32.KeyDownInput(win32.KeycodeDown), win32.KeyUpInput(win32.KeycodeDown))
}
}

func overwriteYololCode(code string) {
lines := strings.Split(code, "\n")
for _, line := range lines {
deleteLine()
win32.SendString(line)
time.Sleep(typeDelay)
win32.SendInput(win32.KeyDownInput(win32.KeycodeDown), win32.KeyUpInput(win32.KeycodeDown))
}
}

func deleteAllLines() {
for i := 0; i < 20; i++ {
deleteLine()
win32.SendInput(win32.KeyDownInput(win32.KeycodeDown), win32.KeyUpInput(win32.KeycodeDown))
}
}

func deleteLine() {
win32.SendInput(win32.KeyDownInput(win32.KeycodeCtrl), win32.KeyDownInput('A'))
time.Sleep(typeDelay)
win32.SendInput(win32.KeyUpInput('A'), win32.KeyUpInput(win32.KeycodeCtrl))
win32.SendInput(win32.KeyDownInput(win32.KeycodeBackspace), win32.KeyUpInput(win32.KeycodeBackspace))
time.Sleep(typeDelay)
}
7 changes: 4 additions & 3 deletions pkg/langserver/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (
var NotFoundError = fmt.Errorf("File not found in cache")

type Cache struct {
Files map[lsp.DocumentURI]string
Diagnostics map[lsp.DocumentURI]DiagnosticResults
Lock *sync.Mutex
Files map[lsp.DocumentURI]string
Diagnostics map[lsp.DocumentURI]DiagnosticResults
Lock *sync.Mutex
LastOpenedYololFile lsp.DocumentURI
}

type DiagnosticResults struct {
Expand Down
26 changes: 25 additions & 1 deletion pkg/langserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"os"
"runtime"
"strings"

"github.com/dbaumgarten/yodk/pkg/jsonrpc2"
"github.com/dbaumgarten/yodk/pkg/lsp"
Expand All @@ -15,12 +16,18 @@ type LangServer struct {
settings *Settings
}

func Run(ctx context.Context, stream jsonrpc2.Stream, opts ...interface{}) error {
func Run(ctx context.Context, stream jsonrpc2.Stream, enableHotkeys bool, opts ...interface{}) error {
s := &LangServer{}
conn, client := lsp.RunServer(ctx, stream, s, opts...)
s.client = client
s.cache = NewCache()
s.settings = DefaultSettings()

if enableHotkeys {
// Register the global hotkeys
s.ListenForHotkeys()
}

return conn.Wait(ctx)
}

Expand All @@ -43,6 +50,14 @@ func callingFunctionName() string {
return caller.Name()
}

func (ls *LangServer) storeLastOpenedScript(uri string) {
if strings.HasSuffix(uri, ".yolol") {
ls.cache.Lock.Lock()
ls.cache.LastOpenedYololFile = lsp.DocumentURI(uri)
ls.cache.Lock.Unlock()
}
}

func (ls *LangServer) Initialize(ctx context.Context, params *lsp.InitializeParams) (*lsp.InitializeResult, error) {
return &lsp.InitializeResult{
Capabilities: lsp.ServerCapabilities{
Expand Down Expand Up @@ -80,10 +95,19 @@ func (ls *LangServer) Symbols(ctx context.Context, params *lsp.WorkspaceSymbolPa
return nil, unsupported()
}
func (ls *LangServer) ExecuteCommand(ctx context.Context, params *lsp.ExecuteCommandParams) (interface{}, error) {
if params.Command == "activeDocument" {
if len(params.Arguments) == 1 {
if argstr, is := params.Arguments[0].(string); is {
ls.storeLastOpenedScript(argstr)
return nil, nil
}
}
}
return nil, unsupported()
}
func (ls *LangServer) DidOpen(ctx context.Context, params *lsp.DidOpenTextDocumentParams) error {
ls.cache.Set(params.TextDocument.URI, params.TextDocument.Text)
ls.storeLastOpenedScript(string(params.TextDocument.URI))
ls.Diagnose(ctx, params.TextDocument.URI)
return nil
}
Expand Down
101 changes: 101 additions & 0 deletions pkg/langserver/win32/hotkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// +build windows

package win32

import (
"bytes"
"context"
"fmt"
"runtime"
"unsafe"
)

// most of the code is taken from: https://stackoverflow.com/questions/38646794/implement-a-global-hotkey-in-golang#38954281

// Modifiers for Hotkeys
const (
ModAlt = 1 << iota
ModCtrl
ModShift
ModWin
)

var (
reghotkey = user32.MustFindProc("RegisterHotKey")
getmsg = user32.MustFindProc("GetMessageW")
)

// Hotkey represents a key-combination pressed by a user
type Hotkey struct {
// Id, must be unique for each registered hotkey
ID int // Unique id
// Modifiers is a bitmask containing modifiers for the hotkey
Modifiers int // Mask of modifiers
// KeyCode is the keycode for the hotkey
KeyCode int // Key code, e.g. 'A'
}

// String returns a human-friendly display name of the hotkey
// such as "Hotkey[Id: 1, Alt+Ctrl+O]"
func (h Hotkey) String() string {
mod := &bytes.Buffer{}
if h.Modifiers&ModAlt != 0 {
mod.WriteString("Alt+")
}
if h.Modifiers&ModCtrl != 0 {
mod.WriteString("Ctrl+")
}
if h.Modifiers&ModShift != 0 {
mod.WriteString("Shift+")
}
if h.Modifiers&ModWin != 0 {
mod.WriteString("Win+")
}
return fmt.Sprintf("Hotkey[Id: %d, %s%c]", h.ID, mod, h.KeyCode)
}

// HotkeyHandler is the callback for registered hotkeys
type HotkeyHandler func(Hotkey)

type msg struct {
HWND uintptr
UINT uintptr
WPARAM int16
LPARAM int64
DWORD int32
POINT struct{ X, Y int64 }
}

// ListenForHotkeys registers an listens for the given global Hotkeys. If a hotkey is pressed, the hendler function is executed
// This function blocks, so it shoue have it's own goroutine
func ListenForHotkeys(ctx context.Context, handler HotkeyHandler, hotkeys ...*Hotkey) error {

runtime.LockOSThread()
defer runtime.UnlockOSThread()

hotkeymap := make(map[int16]*Hotkey)
for _, v := range hotkeys {
hotkeymap[int16(v.ID)] = v
r1, _, err := reghotkey.Call(
0, uintptr(v.ID), uintptr(v.Modifiers), uintptr(v.KeyCode))
if r1 != 1 {
return err
}
}

for {
if ctx != nil && ctx.Err() != nil {
return nil
}
var msg = &msg{}
getmsg.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)

// Registered id is in the WPARAM field:
if id := msg.WPARAM; id != 0 {
hk, exists := hotkeymap[id]
if exists {
handler(*hk)
}
}
}
}
Loading

0 comments on commit d83340b

Please sign in to comment.