376 lines
10 KiB
Python
376 lines
10 KiB
Python
import os
|
||
import os.path
|
||
import tempfile
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
import requests
|
||
from talon import Context, Module, actions, app, clip, ui
|
||
|
||
# Courtesy of https://github.com/anonfunc/talon-user/blob/master/apps/jetbrains.py
|
||
|
||
# Each IDE gets its own port, as otherwise you wouldn't be able
|
||
# to run two at the same time and switch between them.
|
||
# Note that MPS and IntelliJ ultimate will conflict...
|
||
port_mapping = {
|
||
"com.google.android.studio": 8652,
|
||
"com.jetbrains.AppCode": 8655,
|
||
"com.jetbrains.CLion": 8657,
|
||
"com.jetbrains.datagrip": 8664,
|
||
"com.jetbrains.goland-EAP": 8659,
|
||
"com.jetbrains.goland": 8659,
|
||
"com.jetbrains.intellij-EAP": 8653,
|
||
"com.jetbrains.intellij.ce": 8654,
|
||
"com.jetbrains.intellij": 8653,
|
||
"com.jetbrains.PhpStorm": 8662,
|
||
"com.jetbrains.pycharm": 8658,
|
||
"com.jetbrains.rider": 8660,
|
||
"com.jetbrains.rubymine": 8661,
|
||
"com.jetbrains.rubymine-EAP": 8661,
|
||
"com.jetbrains.WebStorm": 8663,
|
||
"google-android-studio": 8652,
|
||
"idea64.exe": 8653,
|
||
"IntelliJ IDEA": 8653,
|
||
"jetbrains-appcode": 8655,
|
||
"jetbrains-clion": 8657,
|
||
"jetbrains-datagrip": 8664,
|
||
"jetbrains-goland-eap": 8659,
|
||
"jetbrains-goland": 8659,
|
||
"jetbrains-idea-ce": 8654,
|
||
"jetbrains-idea-eap": 8653,
|
||
"jetbrains-idea": 8653,
|
||
"jetbrains-phpstorm": 8662,
|
||
"jetbrains-pycharm-ce": 8658,
|
||
"jetbrains-pycharm": 8658,
|
||
"jetbrains-rider": 8660,
|
||
"JetBrains Rider": 8660,
|
||
"jetbrains-rubymine": 8661,
|
||
"jetbrains-rubymine-eap": 8661,
|
||
"jetbrains-studio": 8652,
|
||
"jetbrains-webstorm": 8663,
|
||
"RubyMine": 8661,
|
||
"RubyMine-EAP": 8661,
|
||
"PyCharm": 8658,
|
||
"pycharm64.exe": 8658,
|
||
"WebStorm": 8663,
|
||
"webstorm64.exe": 8663,
|
||
}
|
||
|
||
|
||
def _get_nonce(port: int, file_prefix: str) -> Optional[str]:
|
||
file_name = file_prefix + str(port)
|
||
try:
|
||
with open(os.path.join(tempfile.gettempdir(), file_name)) as fh:
|
||
return fh.read()
|
||
except FileNotFoundError:
|
||
try:
|
||
with open(Path.home() / file_name) as fh:
|
||
return fh.read()
|
||
except FileNotFoundError:
|
||
print(f"Could not find {file_name} in tmp or home")
|
||
return None
|
||
except OSError as e:
|
||
print(e)
|
||
return None
|
||
|
||
|
||
def send_idea_command(cmd: str) -> str:
|
||
active_app = ui.active_app()
|
||
bundle = active_app.bundle or active_app.name
|
||
port = port_mapping.get(bundle, None)
|
||
if not port:
|
||
raise Exception(f"unknown application {bundle}")
|
||
nonce = _get_nonce(port, ".vcidea_") or _get_nonce(port, "vcidea_")
|
||
if not nonce:
|
||
raise FileNotFoundError(f"Couldn't find IDEA nonce file for port {port}")
|
||
|
||
response = requests.get(
|
||
f"http://localhost:{port}/{nonce}/{cmd}",
|
||
proxies={"http": None, "https": None},
|
||
timeout=(0.05, 3.05),
|
||
)
|
||
response.raise_for_status()
|
||
return response.text
|
||
|
||
|
||
def get_idea_location() -> list[str]:
|
||
return send_idea_command("location").split()
|
||
|
||
|
||
ctx = Context()
|
||
mod = Module()
|
||
|
||
mod.apps.jetbrains = "app.name: /jetbrains/"
|
||
mod.apps.jetbrains = "app.name: CLion"
|
||
mod.apps.jetbrains = "app.name: IntelliJ IDEA"
|
||
mod.apps.jetbrains = "app.name: PhpStorm"
|
||
mod.apps.jetbrains = "app.name: PyCharm"
|
||
mod.apps.jetbrains = "app.name: WebStorm"
|
||
mod.apps.jetbrains = "app.name: RubyMine"
|
||
mod.apps.jetbrains = "app.name: RubyMine-EAP"
|
||
mod.apps.jetbrains = "app.name: DataGrip"
|
||
mod.apps.jetbrains = """
|
||
os: mac
|
||
and app.bundle: com.google.android.studio
|
||
"""
|
||
# windows
|
||
mod.apps.jetbrains = r"app.exe: /^idea64\.exe$/i"
|
||
mod.apps.jetbrains = r"app.exe: /^PyCharm64\.exe$/i"
|
||
mod.apps.jetbrains = r"app.exe: /^webstorm64\.exe$/i"
|
||
mod.apps.jetbrains = """
|
||
os: mac
|
||
and app.bundle: com.jetbrains.pycharm
|
||
os: mac
|
||
and app.bundle: com.jetbrains.rider
|
||
"""
|
||
mod.apps.jetbrains = r"""
|
||
os: windows
|
||
and app.name: JetBrains Rider
|
||
os: windows
|
||
and app.exe: /^rider64\.exe$/i
|
||
"""
|
||
|
||
|
||
@mod.action_class
|
||
class Actions:
|
||
def idea(commands: str):
|
||
"""Send a command to Jetbrains product"""
|
||
command_list = commands.split(",")
|
||
try:
|
||
for cmd in command_list:
|
||
if cmd:
|
||
send_idea_command(cmd.strip())
|
||
actions.sleep(0.1)
|
||
except Exception as e:
|
||
app.notify(e)
|
||
raise
|
||
|
||
def idea_grab(times: int):
|
||
"""Copies specified number of words to the left"""
|
||
old_clip = clip.get()
|
||
try:
|
||
original_line, original_column = get_idea_location()
|
||
for _ in range(times):
|
||
send_idea_command("action EditorSelectWord")
|
||
send_idea_command("action EditorCopy")
|
||
send_idea_command(f"goto {original_line} {original_column}")
|
||
send_idea_command("action EditorPaste")
|
||
finally:
|
||
clip.set(old_clip)
|
||
|
||
|
||
ctx.matches = r"""
|
||
app: jetbrains
|
||
"""
|
||
|
||
|
||
@ctx.action_class("app")
|
||
class AppActions:
|
||
def tab_next():
|
||
actions.user.idea("action NextTab")
|
||
|
||
def tab_previous():
|
||
actions.user.idea("action PreviousTab")
|
||
|
||
def tab_close():
|
||
actions.user.idea("action CloseContent")
|
||
|
||
def tab_reopen():
|
||
actions.user.idea("action ReopenClosedTab")
|
||
|
||
|
||
@ctx.action_class("code")
|
||
class CodeActions:
|
||
# talon code actions
|
||
def toggle_comment():
|
||
actions.user.idea("action CommentByLineComment")
|
||
|
||
|
||
@ctx.action_class("edit")
|
||
class EditActions:
|
||
# talon edit actions
|
||
def copy():
|
||
actions.user.idea("action EditorCopy")
|
||
|
||
def cut():
|
||
actions.user.idea("action EditorCut")
|
||
|
||
def delete():
|
||
actions.user.idea("action EditorBackSpace")
|
||
|
||
def paste():
|
||
actions.user.idea("action EditorPaste")
|
||
|
||
def find_next():
|
||
actions.user.idea("action FindNext")
|
||
|
||
def find_previous():
|
||
actions.user.idea("action FindPrevious")
|
||
|
||
def find(text: str = None):
|
||
actions.user.idea("action Find")
|
||
if text:
|
||
actions.insert(text)
|
||
|
||
def line_clone():
|
||
actions.user.idea("action EditorDuplicate")
|
||
|
||
def line_swap_down():
|
||
actions.user.idea("action MoveLineDown")
|
||
|
||
def line_swap_up():
|
||
actions.user.idea("action MoveLineUp")
|
||
|
||
def indent_more():
|
||
actions.user.idea("action EditorIndentLineOrSelection")
|
||
|
||
def indent_less():
|
||
actions.user.idea("action EditorUnindentSelection")
|
||
|
||
def select_line(n: int = None):
|
||
actions.user.idea("action EditorSelectLine")
|
||
|
||
def select_word():
|
||
actions.user.idea("action EditorSelectWord")
|
||
|
||
def select_all():
|
||
actions.user.idea("action $SelectAll")
|
||
|
||
def file_start():
|
||
actions.user.idea("action EditorTextStart")
|
||
|
||
def file_end():
|
||
actions.user.idea("action EditorTextEnd")
|
||
|
||
def extend_file_start():
|
||
actions.user.idea("action EditorTextStartWithSelection")
|
||
|
||
def extend_file_end():
|
||
actions.user.idea("action EditorTextEndWithSelection")
|
||
|
||
def extend_word_left():
|
||
actions.user.idea("action EditorPreviousWordWithSelection")
|
||
|
||
def extend_word_right():
|
||
actions.user.idea("action EditorNextWordWithSelection")
|
||
|
||
def jump_line(n: int):
|
||
actions.user.idea(f"goto {n} 0")
|
||
# move the cursor to the first nonwhite space character of the line
|
||
actions.user.idea("action EditorLineEnd")
|
||
actions.user.idea("action EditorLineStart")
|
||
|
||
|
||
@ctx.action_class("win")
|
||
class WinActions:
|
||
def filename() -> str:
|
||
title: str = actions.win.title()
|
||
result = title.split()
|
||
|
||
# iterate over reversed result
|
||
# to support titles such as
|
||
# Class.Library2 – a.js [.workspace]
|
||
for word in reversed(result):
|
||
if not word.startswith("[") and "." in word:
|
||
return word
|
||
|
||
return ""
|
||
|
||
|
||
@ctx.action_class("user")
|
||
class UserActions:
|
||
def tab_jump(number: int):
|
||
# depends on plugin GoToTabs
|
||
if number < 10:
|
||
actions.user.idea(f"action GoToTab{number}")
|
||
|
||
def extend_until_line(line: int):
|
||
actions.user.idea(f"extend {line}")
|
||
|
||
def select_range(line_start: int, line_end: int):
|
||
# if it's a single line, select the entire thing including the ending new-line5
|
||
if line_start == line_end:
|
||
actions.user.idea(f"goto {line_start} 0")
|
||
actions.user.idea("action EditorSelectLine")
|
||
else:
|
||
actions.user.idea(f"range {line_start} {line_end}")
|
||
|
||
def extend_camel_left():
|
||
actions.user.idea("action EditorPreviousWordInDifferentHumpsModeWithSelection")
|
||
|
||
def extend_camel_right():
|
||
actions.user.idea("action EditorNextWordInDifferentHumpsModeWithSelection")
|
||
|
||
def camel_left():
|
||
actions.user.idea("action EditorPreviousWordInDifferentHumpsMode")
|
||
|
||
def camel_right():
|
||
actions.user.idea("action EditorNextWordInDifferentHumpsMode")
|
||
|
||
def command_search(command: str = ""):
|
||
actions.user.idea("action GotoAction")
|
||
if command != "":
|
||
actions.insert(command)
|
||
|
||
def line_clone(line: int):
|
||
actions.user.idea(f"clone {line}")
|
||
|
||
# multi-cursor tag functions
|
||
def multi_cursor_enable():
|
||
actions.skip()
|
||
|
||
def multi_cursor_disable():
|
||
actions.key("escape")
|
||
|
||
def multi_cursor_add_above():
|
||
actions.user.idea("action EditorCloneCaretAbove")
|
||
|
||
def multi_cursor_add_below():
|
||
actions.user.idea("action EditorCloneCaretBelow")
|
||
|
||
def multi_cursor_select_fewer_occurrences():
|
||
actions.user.idea("action UnselectPreviousOccurrence")
|
||
|
||
def multi_cursor_select_more_occurrences():
|
||
actions.user.idea("action SelectNextOccurrence")
|
||
|
||
# def multi_cursor_skip_occurrence():
|
||
def multi_cursor_select_all_occurrences():
|
||
actions.user.idea("action SelectAllOccurrences")
|
||
|
||
def multi_cursor_add_to_line_ends():
|
||
actions.user.idea("action EditorAddCaretPerSelectedLine")
|
||
|
||
# splits tag functions
|
||
# def split_window_right():
|
||
# actions.user.idea("action OpenInRightSplit")
|
||
# def split_window_left():
|
||
# def split_window_down():
|
||
# def split_window_up():
|
||
def split_window_vertically():
|
||
actions.user.idea("action SplitVertically")
|
||
|
||
def split_window_horizontally():
|
||
actions.user.idea("action SplitHorizontally")
|
||
|
||
def split_flip():
|
||
actions.user.idea("action ChangeSplitOrientation")
|
||
|
||
def split_maximize():
|
||
actions.key("ctrl-shift-f12")
|
||
|
||
def split_reset():
|
||
actions.key("shift-f12")
|
||
|
||
# def split_window():
|
||
def split_clear():
|
||
actions.user.idea("action Unsplit")
|
||
|
||
def split_clear_all():
|
||
actions.user.idea("action UnsplitAll")
|
||
|
||
def split_next():
|
||
actions.user.idea("action NextSplitter")
|
||
|
||
# def split_last():
|
||
# def split_number(index: int):
|