Skip to content

Commit

Permalink
feat: show only installed editors, detect editor of repos by language…
Browse files Browse the repository at this point in the history
…, support projects in mono repos as separate units to open (#13)

* add more editors, detect editor by language

* review tasks

* review tasks

* review tasks

* review tasks

* fix bug setting editors

---------

Co-authored-by: Lukas Kürzdörfer <[email protected]>
  • Loading branch information
lukas-kd and Lukas Kürzdörfer authored Jan 17, 2025
1 parent e72ec29 commit 86d54c0
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 8 deletions.
Binary file added images/goland.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/pycharm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/rider.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/rustrover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/webstorm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 141 additions & 8 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import subprocess
import shutil

from ulauncher.api.client.EventListener import EventListener
from ulauncher.api.client.Extension import Extension
Expand All @@ -12,42 +13,137 @@
PreferencesEvent,
PreferencesUpdateEvent)
from ulauncher.api.shared.item.ExtensionResultItem import ExtensionResultItem
import json

class CodeEditor(str):
CODE = "code"
WEBSTORM = "webstorm"
PYCHARM = "pycharm"
INTELLIJ = "intellij"
GOLAND = "goland"
RUSTROVER = "rustrover"
RIDER = "rider"

class CodeEditorResolver:
DEFAULT_EDITOR = CodeEditor.CODE

LANGUAGE_FILE_EXTENSIONS = {
'python': ['.py'],
'javascript': ['.js', '.jsx', '.ts', '.tsx'],
'java': ['.java', '.kt', '.kts'],
'csharp': ['.cs', '.sln'],
'go': ['.go'],
'rust': ['.rs'],
}

def detect_language(self, folder_path: str) -> set:
language_counts = {language: 0 for language in CodeEditorResolver.LANGUAGE_FILE_EXTENSIONS}

for _, _, files in os.walk(folder_path):
for file in files:
_, ext = os.path.splitext(file)
for language, extensions in CodeEditorResolver.LANGUAGE_FILE_EXTENSIONS.items():
if ext in extensions:
language_counts[language] += 1

languages_with_more_than_10_files = [lang for lang, count in language_counts.items() if count > 10]

if len(languages_with_more_than_10_files) > 1:
return "unknown"

most_common_language = max(language_counts, key=language_counts.get, default=None)

return most_common_language if language_counts[most_common_language] > 0 else "unknown"

def get_editor(self, folder_path: str, lang_editor_map: dict[str,str]) -> str:
language = self.detect_language(folder_path)
editor = lang_editor_map[language] if language in lang_editor_map else self.DEFAULT_EDITOR
return editor

class RepoOpenerExtension(Extension):
editor_resolver: CodeEditorResolver = None

language_editor_map = {
'python': CodeEditor.PYCHARM,
'javascript': CodeEditor.WEBSTORM,
'java': CodeEditor.INTELLIJ,
'go': CodeEditor.GOLAND,
'rust': CodeEditor.RUSTROVER,
'csharp': CodeEditor.RIDER,
}

tool_command_map = {
'code': None,
'intellij': None
'intellij': None,
'rider': None,
'pycharm': None,
'webstorm': None,
'goland': None,
'rustrover': None,
}

shorthand_tool_map = {
'c': 'code',
'i': 'intellij'
'i': 'intellij',
'r': 'rider',
'p': 'pycharm',
'w': 'webstorm',
'g': 'goland',
'rr': 'rustrover'
}

search_path = None
mono_repositories: str = None

repos = []

def __init__(self):
super(RepoOpenerExtension, self).__init__()
self.editor_resolver = CodeEditorResolver()
self.subscribe(KeywordQueryEvent, KeywordQueryEventListener())
self.subscribe(PreferencesEvent, PreferencesEventListener())
self.subscribe(PreferencesUpdateEvent,
PreferencesUpdateEventListener())
self.subscribe(ItemEnterEvent, ItemEnterEventListener())

def resolve_installed_tools(self):
for tool, path in self.tool_command_map.items():
self.tool_command_map[tool] = shutil.which(path if path is not None else tool)

def find_and_store_local_git_repos(self):
root_path = self.search_path
repos = [{"path": folder_entry[0],
"subdirs": folder_entry[1],
"name": os.path.basename(folder_entry[0]),
"tool": "intellij" if ".idea" in folder_entry[1] else "code"
"tool": self.get_editor(folder_entry[0])
}
for folder_entry in os.walk(os.path.expandvars(root_path)) if ".git" in folder_entry[1]]
if self.mono_repositories:
for m in [x for x in self.mono_repositories.split(';') if x]:
parts = m.split(':')
if len(parts) != 2:
continue
repo_name = parts[0]
repo = next((repo for repo in repos if repo["name"].lower() == repo_name.lower()), None)
if repo is None:
continue
for f in [x for x in parts[1].split(',') if x]:
folder_path = os.path.join(repo["path"], f)
if os.path.isdir(folder_path):
repos.append({
"path": folder_path,
"name": repo["name"] + " - " + f,
"tool": self.get_editor(folder_path)
})

self.repos = repos

def get_editor(self, folder_path: str) -> str:
editor = self.editor_resolver.get_editor(folder_path, self.language_editor_map)
if self.tool_command_map[editor] is not None:
return editor
return self.editor_resolver.DEFAULT_EDITOR


def open_repo(self, repo):
path = repo["path"]
tool = self.tool_command_map[repo["tool"]]
Expand Down Expand Up @@ -75,14 +171,15 @@ def on_event(self, event, extension: RepoOpenerExtension):
tool_alias = splitted_query[0]
tool = extension.shorthand_tool_map.get(tool_alias)
search_term = splitted_query[1]
if not tool:
if tool is None or extension.tool_command_map[tool] is None:
name = "The given tool shorthand does not exist."
description = "Use another shorthand."
return RenderResultListAction([self.gen_result_item(name, description)])

repos = [repo.copy() for repo in extension.repos if search_term.lower()
in repo["name"].lower()]


if has_custom_tool:
for repo in repos:
repo["tool"] = tool
Expand Down Expand Up @@ -121,6 +218,7 @@ def on_event(self, event, extension: RepoOpenerExtension):
action = event_data["action"]

if action == "refresh":
extension.resolve_installed_tools()
extension.find_and_store_local_git_repos()
elif action == "open":
repo = event_data["repo"]
Expand All @@ -132,20 +230,55 @@ def on_event(self, event, extension: RepoOpenerExtension):
extension.search_path = event.preferences["search_path"]
extension.tool_command_map["code"] = event.preferences["vscode_command"]
extension.tool_command_map["intellij"] = event.preferences["intellij_command"]
extension.tool_command_map["rider"] = event.preferences["rider_command"]
extension.tool_command_map["pycharm"] = event.preferences["pycharm_command"]
extension.tool_command_map["webstrom"] = event.preferences["webstrom_command"]
extension.tool_command_map["goland"] = event.preferences["goland_command"]
extension.tool_command_map["rustrover"] = event.preferences["rustrover_command"]
extension.mono_repositories = event.preferences["mono_repositories"]
editor_map = event.preferences["language_editor_map"]
for line in editor_map.split(','):
if line.strip():
parts = line.split(':')
if len(parts) == 2:
lang = parts[0].strip().lower()
editor = parts[1].strip().lower()
extension.language_editor_map[lang] = CodeEditor(editor)
extension.resolve_installed_tools()
extension.find_and_store_local_git_repos()


class PreferencesUpdateEventListener(EventListener):
def on_event(self, event, extension: RepoOpenerExtension):
if event.id == "search_path":
extension.search_path = event.new_value
extension.find_and_store_local_git_repos()
elif event.id == "mono_repositories":
extension.mono_repositories = event.new_value
elif event.id == "language_editor_map":
editor_map = event.new_value
for line in editor_map.split(','):
if line.strip():
parts = line.split(':')
if len(parts) == 2:
lang = parts[0].strip().lower()
editor = parts[1].strip().lower()
extension.language_editor_map[lang] = CodeEditor(editor)
elif event.id == "vscode_command":
extension.tool_command_map["code"] = event.new_value
elif event.id == "intellij_command":
extension.tool_command_map["intellij"] = event.new_value
else:
pass
elif event.id == "rider_command":
extension.tool_command_map["rider"] = event.new_value
elif event.id == "webstrom_command":
extension.tool_command_map["webstrom"] = event.new_value
elif event.id == "pycharm_command":
extension.tool_command_map["pycharm"] = event.new_value
elif event.id == "goland_command":
extension.tool_command_map["goland"] = event.new_value
elif event.id == "rustrover_command":
extension.tool_command_map["rustrover"] = event.new_value
extension.resolve_installed_tools()
extension.find_and_store_local_git_repos()


if __name__ == "__main__":
Expand Down
49 changes: 49 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@
"description": "The path under which the repositories should be searched. The use of environment variables is allowed.",
"default_value": "$HOME/repos"
},
{
"id": "mono_repositories",
"type": "input",
"name": "Repositories with multiple codebases.",
"description": "List to multiply repostories into specific paths. Example: \"code1:frontend,backend\" will split the repository \"code1\" into 3 items (code1, code1-fronted, code1-backend). Multiple Items will be seperated by \";\"",
"default_value": ""
},
{
"id": "language_editor_map",
"type": "input",
"name": "Language to Editor mapping",
"description": "A mapping between the programming language and the default ide to open. Each mapping defines one mapping between language and editor. Multiple mappings are seperated by ','. The language and editor are seperated by ':'. Example: 'csharp:rider,javascript:webstorm'",
"default_value": "python:pycharm,javascript:webstorm,java:intellij,go:goland,rust:rustrover,csharp:rider"
},
{
"id": "intellij_command",
"type": "input",
Expand All @@ -32,6 +46,41 @@
"name": "Visual Studio Code command",
"description": "The command to open Visual Studio Code. You can use the normal or the insiders version.",
"default_value": "code-insiders"
},
{
"id": "rider_command",
"type": "input",
"name": "Rider command",
"description": "The command to open Rider.",
"default_value": "rider"
},
{
"id": "webstrom_command",
"type": "input",
"name": "WebStorm command",
"description": "The command to open WebStorm.",
"default_value": "webstorm"
},
{
"id": "pycharm_command",
"type": "input",
"name": "PyCharm command",
"description": "The command to open PyCharm. You can use the community or professional version.",
"default_value": "pycharm"
},
{
"id": "goland_command",
"type": "input",
"name": "GoLand command",
"description": "The command to open GoLand.",
"default_value": "goland"
},
{
"id": "rustrover_command",
"type": "input",
"name": "RustRover command",
"description": "The command to open GoLand.",
"default_value": "rustrover"
}
]
}

0 comments on commit 86d54c0

Please sign in to comment.