diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f6f35d7..0911027 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -28,7 +28,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -r requirements.txt - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/.vscode/launch.json b/.vscode/launch.json index e37d4a6..7b71c6f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Python: Current File", + "name": "Python: Cupcake basic example", "type": "python", "request": "launch", "program": "examples/basic.py", diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fd19597 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + + +## Unreleased + +[Compare with latest](https://github.com/billyeatcookies/cupcake/compare/v0.2.0-alpha...HEAD) + +### Added + +- Add Screenshots ([4a904b9](https://github.com/billyeatcookies/cupcake/commit/4a904b9822d0b1671df9544de5d3d19e5370e3a8) by Billy). +- Add quick start ([328df21](https://github.com/billyeatcookies/cupcake/commit/328df2156bc1480b786d455303072d969e6f9e06) by Billy). +- Add feature list ([1ee937e](https://github.com/billyeatcookies/cupcake/commit/1ee937e2e6cf6ae71b053dbd2e3d5e0f53273f9f) by Billy). +- Add docs ([f7cc3d6](https://github.com/billyeatcookies/cupcake/commit/f7cc3d61d3907b619b39dde4b6bca9376ea41925) by Billy). +- Add contributing guidelines ([07a8bd0](https://github.com/billyeatcookies/cupcake/commit/07a8bd07916c932d08e9b22a6033eae07d1a134d) by Billy). +- Add poetry for packaging and dependancy management ([9358b0e](https://github.com/billyeatcookies/cupcake/commit/9358b0e5ff219b201d4c39031c9e2b5026880ac7) by Billy). +- Add find-replace to feature list ([1f18d8c](https://github.com/billyeatcookies/cupcake/commit/1f18d8cf0cb458e0c5adf01f6548638eb8248dad) by Billy). +- Add links ([df7b6e7](https://github.com/billyeatcookies/cupcake/commit/df7b6e77b800dc33b89af32cf9e81a9f051aa3fa) by Billy). +- Add images ([d55cc54](https://github.com/billyeatcookies/cupcake/commit/d55cc54e2b99581d8f4993225e4e375c0c1f2feb) by Billy). + +### Fixed + +- fix: Get the editor running fine ([afb66e9](https://github.com/billyeatcookies/cupcake/commit/afb66e9f59c6fdb5115583948b026374fddc3083) by Billy). +- Fix line selection from line numbers ([c432bd8](https://github.com/billyeatcookies/cupcake/commit/c432bd8048313062ab3a53427577722448514773) by Billy). +- Fix typo ([3481940](https://github.com/billyeatcookies/cupcake/commit/3481940e0c2140e14e157f46faa7c22e683d4b50) by Billy). +- Fix in readme ([4379449](https://github.com/billyeatcookies/cupcake/commit/4379449b689efdee7f0537796c34ff73b3c5f45b) by Billy). +- Fix tests, update readme ([0a42d23](https://github.com/billyeatcookies/cupcake/commit/0a42d23a2612ec5f4d2f377c40f2d40fbf52711e) by Billy). +- Fix tests and attribute errors ([68afd28](https://github.com/billyeatcookies/cupcake/commit/68afd285579db1a98b8c2f682dd806656ecbd342) by Billy). +- Fix autocomplete borders ([8b4adc1](https://github.com/billyeatcookies/cupcake/commit/8b4adc1eedacbd0ad430090474228b66b88c7b34) by Billy). +- fix conflicts ([07aba0c](https://github.com/billyeatcookies/cupcake/commit/07aba0cee2eb2db98ae538e9ff3c51ba78c0331c) by cid0rz). +- Fix entry indixes ([9908634](https://github.com/billyeatcookies/cupcake/commit/99086347e3b1e919a90bcbb97754c45df3a304c2) by Billy). +- Fix the big issue slowing down editor ([3b3d0c9](https://github.com/billyeatcookies/cupcake/commit/3b3d0c9b061741f8fed561a95180768e729e9193) by billyeatcookies). + +### Changed + +- Change version to 0.5.1 ([e32417b](https://github.com/billyeatcookies/cupcake/commit/e32417ba75199eabb09f47edc938c3a0d82554e6) by Billy). + +### Removed + +- remove docs page ([8cd0f9a](https://github.com/billyeatcookies/cupcake/commit/8cd0f9a850f4d956423131fbf22c9d8b1e1999b8) by Billy). +- remove docs ([5845bf9](https://github.com/billyeatcookies/cupcake/commit/5845bf9708b1597e68c3923cd6d50d530e0d8530) by Billy). +- Remove unnecessary logs for scroll events ([9665e47](https://github.com/billyeatcookies/cupcake/commit/9665e47cb645659061509cd8d162f768b3940b2f) by Billy). +- Remove proposals ([d9a6648](https://github.com/billyeatcookies/cupcake/commit/d9a6648bfb10633ac56e29b41222f93f79077eb7) by Billy). +- Remove clear_selection ([7977a63](https://github.com/billyeatcookies/cupcake/commit/7977a63a863b774df6c1af306521ece23679bc24) by Billy). +- Remove Find Replace feature from list ([13fb9da](https://github.com/billyeatcookies/cupcake/commit/13fb9dacedbd0ada5ef6943ccd252199b434e1cf) by Billy). + + +## [v0.2.0-alpha](https://github.com/billyeatcookies/cupcake/releases/tag/v0.2.0-alpha) - 2022-03-29 + +[Compare with first commit](https://github.com/billyeatcookies/cupcake/compare/2df53a1562c4c349c90554342ff352b567a72754...v0.2.0-alpha) + +### Added + +- Add homepage ([7ed5cc1](https://github.com/billyeatcookies/cupcake/commit/7ed5cc18b655f017914657f4b3127e4600e3b875) by billyeatcookies). +- Add proposals ([30cc23a](https://github.com/billyeatcookies/cupcake/commit/30cc23a8e3bb004aedea1cb7bab37b5f9b973f7e) by Billy). +- Add minimap ([4c98445](https://github.com/billyeatcookies/cupcake/commit/4c984451a96ce4a6105ac72d3e58e3904771106b) by billyeatcookies). +- Add `/docs` ([6f8ceb9](https://github.com/billyeatcookies/cupcake/commit/6f8ceb9ffe3f43e87565c97e5b82820285ae7575) by billyeatcookies). + +### Fixed + +- Fix: autocomplete window not hiding on no results ([400dede](https://github.com/billyeatcookies/cupcake/commit/400dede5718523dbaf4a8b8f6585175b56343aa3) by billyeatcookies). +- Fix highlighting issues ([8a6711f](https://github.com/billyeatcookies/cupcake/commit/8a6711f17784480771cb808055273c5c0ba4c45e) by billyeatcookies). +- Fix resizing issues, auto indentation, zoom, drag n drop ([ef1a3da](https://github.com/billyeatcookies/cupcake/commit/ef1a3da36e459c9167b6dccafa3c3df4fc76111f) by billyeatcookies). + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..26be61c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +billydevbusiness@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a70cf8..beaa898 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,20 +1,33 @@ -# CONTRIBUTING -Thank you for considering to contribute to Cupcake! To keep the code clean and for understanding our naming conventions, here are some guidelines and a list of coding tools used. +

CONTRIBUTING

-## Dependencies -For package management and dependencies this project uses [**poetry**](https://python-poetry.org/docs/#installation) +## Cupcake Developer Guide +Welcome to the Contributing Guidelines for Cupcake. Your contributions and support are greatly appreciated! ๐Ÿงก +> **Note** +> You will need [Git](https://git-scm.com/) installed to follow the steps below! -After installation of poetry, you will be able to install all dependencies of cupcake with the following command -``` -poetry install -``` +## Fixing an issue/adding a feature +Here are the basic steps needed to get set up and contribute a patch. -## Running Tests -Cupcake uses [**pytest**](https://docs.pytest.org/) to run tests. After installation of poetry, you will be able to run the following command -``` -poetry run pytest -``` -Otherwise, you can also run pytest directly -``` -pytest tests +1. [**Fork**](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the Cupcake repository to your GitHub account and get the source code using: + +```bash +git clone https://github.com//Cupcake.git +cd Cupcake ``` +2. We appreciate using a [**Release of Biscuit**](https://github.com/billyeatcookies/Biscuit/releases) itself for development, but you can use your preferred editor. Open the project directory in your preferred environment. +3. **Open an issue** report in the [issue tracker](https://github.com/billyeatcookies/Cupcake/issues) if the issue has not been reported/proposed yet. + +4. Switch to a new local branch where your work for the issue will go (format it `fix-#` or `feat-#` if you are having trouble naming) + ``` + git checkout -b fix-123 main + ``` +5. Once you fixed the issue, try running the related examples, and check no errors are occuring, if everything is ok, commit with a proper message regarding what has been fixed. +6. You can run the changelog command (not mandatory, requires `pip install git-changelog`): + ``` + git-changelog -o CHANGELOG.md + ``` + This will update the changelog, and now commit the changes. +7. Push the branch on your fork on GitHub and create a pull request. Include `fix: #` in the description + ``` + fix: #123, Fix x bug in y module + ``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..dc4243e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +The following versions of Cupcakes are currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 0.20.x | :white_check_mark: | + +## Reporting a Vulnerability + +Go the discussions tab of the repo and you can open a discussion there, and we will look into it! you can expect to get an update on the reported vulnerability within a few days, we will try to fix it quickly if its accepted. diff --git a/cupcake/__init__.py b/cupcake/__init__.py index e9c49d9..06ba644 100644 --- a/cupcake/__init__.py +++ b/cupcake/__init__.py @@ -1,3 +1,114 @@ -__version__ = '0.5.1' +__version__ = '0.25.0' +__version_info__ = tuple([ int(num) for num in __version__.split('.')]) -from .editor import Editor +__all__ = ["Editor", "get_editor", "DiffEditor", "ImageViewer", "TextEditor", "Config", "Languages"] + + +import os +import tkinter as tk +from tkinter.font import Font + +from .config import Config +from .languages import Languages +from .utils import FileType, Frame + +from .breadcrumbs import BreadCrumbs +from .diffeditor import DiffEditor +from .imageviewer import ImageViewer +from .texteditor import TextEditor + +def get_editor(base, path=None, path2=None, diff=False, language=None): + "picks the right editor for the given values" + if diff: + return DiffEditor(base, path, path2, language=language) + + if path and os.path.isfile(path): + if FileType.is_image(path): + return ImageViewer(base, path) + + return TextEditor(base, path, language=language) + + return TextEditor(base, language=language) + + +class Editor(Frame): + """ + Editor class + Picks the right editor based on the path, path2, diff values passed. Supports showing diff, images, text files. + If nothing is passed, empty text editor is opened. + + Attributes + ---------- + path : str + path of the file to be opened + path2 : str + path of file to be opened in diff, required if diff=True is passed + diff : bool + whether this is to be opened in diff editor + language : str + Use the `Languages` enum provided (eg. Languages.PYTHON, Languages.TYPESCRIPT) + This is given priority while picking suitable highlighter. If not passed, guesses from file extension. + dark_mode : str + Sets the editor theme to cupcake dark if True, or cupcake light by default + This is ignored if custom config_file path is passed + config_file : str + path to the custom config (TOML) file, uses theme defaults if not passed + showpath : bool + whether to show the breadcrumbs for editor or not + font : str | Font + Font used in line numbers, text editor, autocomplete. defaults to Consolas(11) + uifont : str | Font + Font used for other UI components (breadcrumbs, trees) + preview_file_callback : function(path) + called when files in breadcrumbs-pathview are single clicked. MUST take an argument (path) + open_file_callback : function(path) + called when files in breadcrumbs-pathview are double clicked. MUST take an argument (path) + + NOTE: All the *tk.Text* methods are available under *Editor.content* (eg. Editor.content.insert, Editor.content.get) + + Methods + ------- + save(path: str=None) + If the content is editable writes to the specified path. + focus() + Gives focus to the content. + """ + def __init__(self, master, + path: str=None, path2: str=None, diff: bool=False, language: str=None, + darkmode=True, config_file: str=None, showpath: bool=True, + font: str|Font=None, uifont: str|Font=None, + preview_file_callback=None, open_file_callback=None, + *args, **kwargs) -> None: + super().__init__(master, *args, **kwargs) + + self.path = path + self.path2 = path2 + self.diff = diff + self.showpath = showpath + self.darkmode = darkmode + self.config_file = config_file + self.preview_file_callback = preview_file_callback + self.open_file_callback = open_file_callback + + self.settings = Config(self, config_file, darkmode, font, uifont) + self.theme = self.settings.theme + + self.config(bg=self.theme.border) + self.grid_columnconfigure(0, weight=1) + + self.content = get_editor(self, path, path2, diff, language) + self.filename = os.path.basename(self.path) if path else None + if path and self.showpath and not diff: + self.breadcrumbs = BreadCrumbs(self, path) + self.grid_rowconfigure(1, weight=1) + self.breadcrumbs.grid(row=0, column=0, sticky=tk.EW, pady=(0, 1)) + self.content.grid(row=1, column=0, sticky=tk.NSEW) + else: + self.grid_rowconfigure(0, weight=1) + self.content.grid(row=0, column=0, sticky=tk.NSEW) + + def save(self, path: str=None) -> None: + self.content.save(path) + + def focus(self) -> None: + self.content.focus() diff --git a/cupcake/breadcrumbs/__init__.py b/cupcake/breadcrumbs/__init__.py new file mode 100644 index 0000000..ee0cb48 --- /dev/null +++ b/cupcake/breadcrumbs/__init__.py @@ -0,0 +1,28 @@ +import os +import tkinter as tk +from .pathview import PathView + +from ..utils import Frame, Menubutton + + +class Item(Menubutton): + def __init__(self, master, path, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.path = path + self.config(pady=3, padx=3, font=self.base.settings.uifont, **self.base.theme.breadcrumbs) + +class BreadCrumbs(Frame): + def __init__(self, master, path=None, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.config(padx=20, bg=self.base.theme.breadcrumbs["background"]) + + self.pathview = PathView(self) + path = os.path.abspath(path).split('\\') + for i, item in enumerate(path): + text = item if item == path[-1] else f"{item} โ€บ" + self.additem("\\".join(path[:i]), text) + + def additem(self, path, text): + btn = Item(self, path, text=text) + btn.bind("", self.pathview.show) + btn.pack(side=tk.LEFT) diff --git a/cupcake/breadcrumbs/pathview.py b/cupcake/breadcrumbs/pathview.py new file mode 100644 index 0000000..c0d55ec --- /dev/null +++ b/cupcake/breadcrumbs/pathview.py @@ -0,0 +1,45 @@ +from ..utils import DirectoryTree, Toplevel + + +class PathView(Toplevel): + def __init__(self, master, width=150, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.width = round(width) + + self.tree = DirectoryTree(self, width=width) + self.tree.pack() + + self.config(pady=1, padx=1, bg=self.base.theme.border) + self.overrideredirect(True) + self.withdraw() + + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(0, weight=1) + self.configure_bindings() + + def configure_bindings(self): + self.bind("", self.hide) + self.bind("", self.hide) + + def get_popup_x(self, width): + return self.winfo_rootx() + int(self.winfo_width() / 2) - int(width / 2) + + def get_popup_y(self): + return self.winfo_rooty() + + def hide(self, *_): + self.withdraw() + + def show(self, e): + self.update_idletasks() + w = e.widget + x = w.winfo_rootx() + y = w.winfo_rooty() + w.winfo_height() + + if not w.path: + return + self.tree.change_path(w.path) + + self.geometry(f"+{x}+{y}") + self.deiconify() + self.focus_set() diff --git a/cupcake/config/__init__.py b/cupcake/config/__init__.py index 17553a4..4705943 100644 --- a/cupcake/config/__init__.py +++ b/cupcake/config/__init__.py @@ -1,9 +1,26 @@ -from tkinter import font +import toml, os +from tkinter.font import Font +from types import SimpleNamespace +from .styles import Style class Config: - def __init__(self, master, *args, **kwargs): - self.master = master - self.font = font.Font( - family="Consolas", - size=11, weight="normal") + def __init__(self, master, config_file=None, darkmode=True, font=None, uifont=None): + self.base = master + self.dir = os.path.dirname(__file__) + + self.stipple = os.path.join(self.dir, 'stipple.xbm') + if not config_file: + config_file = os.path.join(self.dir, 'dark.toml') if darkmode else os.path.join(self.dir, 'light.toml') + + self.font = font or Font(family="Consolas", size=11) + self.uifont = uifont or Font(family="Segoi UI", size=10) + self.load_from(config_file) + + def load_from(self, config_file: str): + self.theme = SimpleNamespace(**toml.load(config_file)) + self.theme.editor = SimpleNamespace(**self.theme.editor) + self.theme.diffeditor = SimpleNamespace(**self.theme.diffeditor) + self.syntax = self.theme.syntax + + self.style = Style(self.base, self) diff --git a/cupcake/config/dark.toml b/cupcake/config/dark.toml new file mode 100644 index 0000000..bb02fb5 --- /dev/null +++ b/cupcake/config/dark.toml @@ -0,0 +1,123 @@ +# Cupcake Dark theme +# author: billyeatcookies +# NOTE: all fields are mandatory and shall only modified, not removed + +accent = "#2aaaff" +background = "#1f1f1f" +foreground = "#cccccc" +border = "#2a2a2a" + + +[editor] +selection = "#264f78" +currentword = "#474747" +currentline = "#282828" +found = "#623315" +foundcurrent = "#9e6a03" + +[autocomplete] +background = "#1f1f1f" +foreground = "#a9a9a9" +activebackground = "#323232" +activeforeground = "#e0e0e0" + +[breadcrumbs] +background = "#1f1f1f" +foreground = "#a9a9a9" +activebackground = "#1f1f1f" +activeforeground = "#e0e0e0" + +[linenumbers] +background = "#1f1f1f" +foreground = "#6e7681" +activebackground = "#1f1f1f" +activeforeground = "#cccccc" + +[minimap] +background = "#1f1f1f" + +[diffeditor] +notexist = "#424242" +added = "#214d28" +addedword = "#204c28" +removed = "#452323" +removedword = "#722827" + +[scrollbar] +background = "#1f1f1f" +activeBackground = "#494d53" + +[tree] +background = "#181818" +foreground = "#cccccc" +activebackground= "#37373d" + + +# following is used for syntax highlighting +[syntax] +"Keyword" = "#569cd6" +"Keyword.Constant" = "#569cd6" +"Keyword.Declaration" = "#569cd6" +"Keyword.Namespace" = "#569cd6" +"Keyword.Pseudo" = "#569cd6" +"Keyword.Reserved" = "#569cd6" +"Keyword.Type" = "#569cd6" + +"Name" = "#4EC9B0" +"Name.Attribute" = "#4EC9B0" +"Name.Builtin" = "#4EC9B0" +"Name.Builtin.Pseudo" = "#4EC9B0" +"Name.Class" = "#4EC9B0" +"Name.Constant" = "#4EC9B0" +"Name.Decorator" = "#4EC9B0" +"Name.Entity" = "#4EC9B0" +"Name.Exception" = "#4EC9B0" +"Name.Function" = "#dcdcaa" +"Name.Function.Magic" = "#dcdcaa" +"Name.Property" = "#4EC9B0" +"Name.Label" = "#4EC9B0" +"Name.Namespace" = "#4EC9B0" +"Name.Other" = "#4EC9B0" +"Name.Tag" = "#4EC9B0" +"Name.Variable" = "#4EC9B0" +"Name.Variable.Class" = "#4EC9B0" +"Name.Variable.Global" = "#4EC9B0" +"Name.Variable.Instance" = "#4EC9B0" +"Name.Variable.Magic" = "#4EC9B0" + +"Literal.String" = "#ce9178" +"Literal.String.Affix" = "#ce9178" +"Literal.String.Backtick" = "#ce9178" +"Literal.String.Char" = "#ce9178" +"Literal.String.Delimiter" = "#ce9178" +"Literal.String.Doc" = "#ce9178" +"Literal.String.Double" = "#ce9178" +"Literal.String.Escape" = "#ce9178" +"Literal.String.Heredoc" = "#ce9178" +"Literal.String.Interpol" = "#ce9178" +"Literal.String.Other" = "#ce9178" +"Literal.String.Regex" = "#ce9178" +"Literal.String.Single" = "#ce9178" +"Literal.String.Symbol" = "#ce9178" + +"Literal.Number" = "#b5cea8" +"Literal.Number.Bin" = "#b5cea8" +"Literal.Number.Float" = "#b5cea8" +"Literal.Number.Hex" = "#b5cea8" +"Literal.Number.Integer" = "#b5cea8" +"Literal.Number.Integer.Long" = "#b5cea8" +"Literal.Number.Oct" = "#b5cea8" + +"Comment" = "#7ca668" +"Comment.Hashbang" = "#7ca668" +"Comment.Multiline" = "#7ca668" +"Comment.Preproc" = "#7ca668" +"Comment.PreprocFile" = "#7ca668" +"Comment.Single" = "#7ca668" +"Comment.Special" = "#7ca668" + +"Operator" = "#cccccc" +"Operator.Word" = "#cccccc" + +"Punctuation" = "#808080" +"Punctuation.Marker" = "#808080" \ No newline at end of file diff --git a/cupcake/config/languages/__init__.py b/cupcake/config/languages/__init__.py deleted file mode 100644 index 20543f1..0000000 --- a/cupcake/config/languages/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .language import Language -from .cpp import CPP diff --git a/cupcake/config/languages/cpp.py b/cupcake/config/languages/cpp.py deleted file mode 100644 index 31271c1..0000000 --- a/cupcake/config/languages/cpp.py +++ /dev/null @@ -1,188 +0,0 @@ -from .language import Language - - -class CPP(Language): - keywords = [ - "abstract", - "amp", - "array", - "auto", - "bool", - "break", - "case", - "catch", - "char", - "class", - "const", - "constexpr", - "const_cast", - "continue", - "cpu", - "decltype", - "default", - "delegate", - "delete", - "do", - "double", - "dynamic_cast", - "each", - "else", - "enum", - "event", - "explicit", - "export", - "extern", - "false", - "final", - "finally", - "float", - "for", - "friend", - "gcnew", - "generic", - "goto", - "if", - "in", - "initonly", - "inline", - "int", - "interface", - "interior_ptr", - "internal", - "literal", - "long", - "mutable", - "namespace", - "new", - "noexcept", - "nullptr", - "__nullptr", - "operator", - "override", - "partial", - "pascal", - "pin_ptr", - "private", - "property", - "protected", - "public", - "ref", - "register", - "reinterpret_cast", - "restrict", - "return", - "safe_cast", - "sealed", - "short", - "signed", - "sizeof", - "static", - "static_assert", - "static_cast", - "struct", - "switch", - "template", - "this", - "thread_local", - "throw", - "tile_static", - "true", - "try", - "typedef", - "typeid", - "typename", - "union", - "unsigned", - "using", - "virtual", - "void", - "volatile", - "wchar_t", - "where", - "while", - - "_asm", - "_based", - "_cdecl", - "_declspec", - "_fastcall", - "_if_exists", - "_if_not_exists", - "_inline", - "_multiple_inheritance", - "_pascal", - "_single_inheritance", - "_stdcall", - "_virtual_inheritance", - "_w64", - - "__abstract", - "__alignof", - "__asm", - "__assume", - "__based", - "__box", - "__builtin_alignof", - "__cdecl", - "__clrcall", - "__declspec", - "__delegate", - "__event", - "__except", - "__fastcall", - "__finally", - "__forceinline", - "__gc", - "__hook", - "__identifier", - "__if_exists", - "__if_not_exists", - "__inline", - "__int128", - "__int16", - "__int32", - "__int64", - "__int8", - "__interface", - "__leave", - "__m128", - "__m128d", - "__m128i", - "__m256", - "__m256d", - "__m256i", - "__m64", - "__multiple_inheritance", - "__newslot", - "__nogc", - "__noop", - "__nounwind", - "__novtordisp", - "__pascal", - "__pin", - "__pragma", - "__property", - "__ptr32", - "__ptr64", - "__raise", - "__restrict", - "__resume", - "__sealed", - "__single_inheritance", - "__stdcall", - "__super", - "__thiscall", - "__try", - "__try_cast", - "__typeof", - "__unaligned", - "__unhook", - "__uuidof", - "__value", - "__virtual_inheritance", - "__w64", - "__wchar_t"] - - strings = ["(\\'(.)\\')", "(\"(.)*\")"] - numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] - comments = ["(//(.)*)", "(\/\\*(.*?)\\*\/)"] diff --git a/cupcake/config/languages/language.py b/cupcake/config/languages/language.py deleted file mode 100644 index 70bd280..0000000 --- a/cupcake/config/languages/language.py +++ /dev/null @@ -1,5 +0,0 @@ -class Language: - keywords: list - strings: list - numbers: list - comments: list diff --git a/cupcake/config/light.toml b/cupcake/config/light.toml new file mode 100644 index 0000000..5adad6a --- /dev/null +++ b/cupcake/config/light.toml @@ -0,0 +1,123 @@ +# Cupcake Dark theme +# author: billyeatcookies +# NOTE: all fields are mandatory and shall only modified, not removed + +accent = "#2aaaff" +background = "white" +foreground = "#424242" +border = "#dfdfdf" + + +[editor] +selection = "#add6ff" +currentword = "#d5d5d5" +currentline = "#eeeeee" +found = "#f8c9ab" +foundcurrent = "#a8ac94" + +[autocomplete] +background = "#f8f8f8" +foreground = "#424242" +activebackground = "#e8e8e8" +activeforeground = "#black" + +[breadcrumbs] +background = "white" +foreground = "#424242" +activebackground = "white" +activeforeground = "black" + +[linenumbers] +background = "white" +foreground = "#727681" +activebackground = "white" +activeforeground = "#171184" + +[minimap] +background = "white" + +[diffeditor] +notexist = "#d3d3d3" +added = "#f8d6d6" +addedword = "#9ecaa6" +removed = "#d3e7d6" +removedword = "#efa6a5" + +[scrollbar] +background = "white" +activeBackground = "#b2b6bc" + +[tree] +background = "#f8f8f8" +foreground = "#424242" +activebackground= "#e8e8e8" + + +# following is used for syntax highlighting +[syntax] +"Keyword" = "#0000ff" +"Keyword.Constant" = "#0000ff" +"Keyword.Declaration" = "#0000ff" +"Keyword.Namespace" = "#0000ff" +"Keyword.Pseudo" = "#0000ff" +"Keyword.Reserved" = "#0000ff" +"Keyword.Type" = "#0000ff" + +"Name" = "#267f99" +"Name.Attribute" = "#267f99" +"Name.Builtin" = "#267f99" +"Name.Builtin.Pseudo" = "#267f99" +"Name.Class" = "#267f99" +"Name.Constant" = "#267f99" +"Name.Decorator" = "#267f99" +"Name.Entity" = "#267f99" +"Name.Exception" = "#267f99" +"Name.Function" = "#795e26" +"Name.Function.Magic" = "#795e26" +"Name.Property" = "#267f99" +"Name.Label" = "#267f99" +"Name.Namespace" = "#267f99" +"Name.Other" = "#267f99" +"Name.Tag" = "#267f99" +"Name.Variable" = "#267f99" +"Name.Variable.Class" = "#267f99" +"Name.Variable.Global" = "#267f99" +"Name.Variable.Instance" = "#267f99" +"Name.Variable.Magic" = "#267f99" + +"Literal.String" = "#b11515" +"Literal.String.Affix" = "#b11515" +"Literal.String.Backtick" = "#b11515" +"Literal.String.Char" = "#b11515" +"Literal.String.Delimiter" = "#b11515" +"Literal.String.Doc" = "#b11515" +"Literal.String.Double" = "#b11515" +"Literal.String.Escape" = "#b11515" +"Literal.String.Heredoc" = "#b11515" +"Literal.String.Interpol" = "#b11515" +"Literal.String.Other" = "#b11515" +"Literal.String.Regex" = "#b11515" +"Literal.String.Single" = "#b11515" +"Literal.String.Symbol" = "#b11515" + +"Literal.Number" = "#098658" +"Literal.Number.Bin" = "#098658" +"Literal.Number.Float" = "#098658" +"Literal.Number.Hex" = "#098658" +"Literal.Number.Integer" = "#098658" +"Literal.Number.Integer.Long" = "#098658" +"Literal.Number.Oct" = "#098658" + +"Comment" = "#098658" +"Comment.Hashbang" = "#098658" +"Comment.Multiline" = "#098658" +"Comment.Preproc" = "#098658" +"Comment.PreprocFile" = "#098658" +"Comment.Single" = "#098658" +"Comment.Special" = "#098658" + +"Operator" = "#cccccc" +"Operator.Word" = "#cccccc" + +"Punctuation" = "#424242" +"Punctuation.Marker" = "#3b3b3b" \ No newline at end of file diff --git a/cupcake/config/stipple.xbm b/cupcake/config/stipple.xbm new file mode 100644 index 0000000..fbc2f00 --- /dev/null +++ b/cupcake/config/stipple.xbm @@ -0,0 +1,8 @@ +#define bd6c6035355741f29cfb4f4e4238905fXdjpZy75KFIildac_width 16 +#define bd6c6035355741f29cfb4f4e4238905fXdjpZy75KFIildac_height 24 +static char bd6c6035355741f29cfb4f4e4238905fXdjpZy75KFIildac_bits[] = { + 0x18, 0x18, 0x0C, 0x0C, 0x06, 0x06, 0x03, 0x03, 0x81, 0x81, 0xC0, 0xC0, + 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0C, 0x0C, 0x06, 0x06, 0x03, 0x03, + 0x81, 0x81, 0xC0, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0C, 0x0C, + 0x06, 0x06, 0x03, 0x03, 0x81, 0x81, 0xC0, 0xC0, 0x60, 0x60, 0x30, 0x30, + }; diff --git a/cupcake/config/styles.py b/cupcake/config/styles.py new file mode 100644 index 0000000..f9ba985 --- /dev/null +++ b/cupcake/config/styles.py @@ -0,0 +1,112 @@ +import tkinter as tk +from tkinter import ttk + + +class Style(ttk.Style): + def __init__(self, master, config, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.base = master + self.config = config + self.theme = config.theme + + self.gen_fileicons() + self.config_treeview() + self.config_tree_scrollbar() + + def config_tree_scrollbar(self): + self.element_create("TreeScrollbar.trough", "from", "clam") + self.element_create("TreeScrollbar.thumb", "from", "clam") + + self.layout("TreeScrollbar", [ + ('TreeScrollbar.trough', { + 'sticky': 'ns', + 'children': [ + ('TreeScrollbar.thumb', { + 'unit': '1', + 'sticky': 'nsew' + }) + ] + }) + ]) + + + bg, highlight = self.theme.scrollbar.values() + self.configure("TreeScrollbar", gripcount=0, background=bg, troughcolor=bg, bordercolor=bg, lightcolor=bg, darkcolor=bg, arrowsize=14) + self.map("TreeScrollbar", background=[('pressed', highlight), ('!disabled', self.theme.border)]) + + self.element_create("EditorScrollbar.trough", "from", "clam") + self.element_create("EditorScrollbar.thumb", "from", "clam") + + self.layout('EditorScrollbar', [ + ('EditorScrollbar.trough', { + 'sticky': 'nsew', + 'children': [ + ('EditorScrollbar.thumb', { + 'sticky': 'nsew' + }) + ] + }) + + ]) + self.configure("EditorScrollbar", gripcount=0, background=bg, troughcolor=bg, bordercolor=bg, lightcolor=bg, darkcolor=bg) + self.map("EditorScrollbar", background=[('pressed', highlight), ('!disabled', self.theme.border)]) + + def config_treeview(self): + ## TREENODE CHEVRONS ----- + self.img_tree_close = tk.PhotoImage("img_tree_close", data=""" + iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d + 2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAALBJREFUGJVjYIACExMTfxMTk8WhoaHMDDgAE4zx69ev4////ze4d+ + /ecgcHBxZsihmROXp6emKsrKx7GRgYrvPy8kYdOHDgD07FhDQwoSu+dOnSq9+/fzszMDBofv78eY2npyc7TsUwDez + s7JEMDAweL1++XAgTx+oRPT09sZ8/fy5nZGTcLiYmFk+Mm6/x8vJGI7sZV2hgKERRTEghigfZ2NgsGRkZLygpKWGE + LwwAAECxSWJ5KCTqAAAAAElFTkSuQmCC""") + self.img_tree_open = tk.PhotoImage("img_tree_open", data=""" + iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d + 2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAALxJREFUGJW10D0KwkAQBeA3wd2EiJ23ELbZYJMTKGJhIZ7G81irCB + 4g5TaBNJ7Bzh92Y/GsItsogvjK4Zs3MMC/IkVRjNu2beq6vr1Dxpi+1nqUkFxrrQ/GmP4HeCC5TgAsATyUUseyLAc + xtNbmSqkdSfHer6QbisiWZJZl2aSqqou1NgewB9Dz3k+bprlK3NItpGm6CCFsYggASYedc3eScxF5hBBOIpLGEABe + zdGFIcm9iMycc+cvv/pjnkvzViGP6ap9AAAAAElFTkSuQmCC""") + self.img_tree_empty = tk.PhotoImage("img_tree_empty", data=""" + iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d + 2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAA5JREFUGJVjYBgFIwgAAAHvAAGLZFZqAAAAAElFTkSuQmCC""") + + self.element_create( + 'Treeitem.nindicator', 'image', self.img_tree_close, + ('user1', '!user2', self.img_tree_open), ('user2', self.img_tree_empty), + sticky='w', width=20) + + self.configure("Treeview", font=self.config.uifont, rowheight=25, **self.theme.tree) + self.map("Treeview", background=[('selected', self.theme.tree["activebackground"])]) + + self.layout('Treeview', [('Treeview.treearea', {'sticky': 'nswe'})]) + self.layout('Treeview.Item', [ + ('Treeitem.padding', { + 'sticky': 'nswe', + 'children': [ + ('Treeitem.nindicator', { + 'side': 'left', 'sticky': '' + }), + ('Treeitem.image', { + 'side': 'left', 'sticky': '' + }), + ('Treeitem.text', { + 'side': 'left', 'sticky': '' + }) + ] + }) + ]) + + def gen_fileicons(self): + self.document_icn = tk.PhotoImage("document", data=""" + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAJ2AAACdgBx6C5rQA + AABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADlSURBVDiNpZGxTgJBFEXPW9aGCRTYWht+wyVEEuq1md + 7Exm/A2NjxFUvBD1CQ7JZWlOhXQCNsoYnPajZk3Zks4VaTmfvOO8nAhRF3SJfa2X2XLwi39ZIi74XtzoOA0eIwUZVVQ+cDu + AHmuTWz+mNUbdGo57HcKiTAc5KVb15AKIU1G4Ux6GMd0grgICIyBX26yw737j5uMZsm2VEBVBUAIeqfbeDLP4PcGmkqujgb + LyDJjsuLDAJJWwFyax6ainV1L8BX9KX6BZHfr7ZDp93KYBCb9f6nfFUYhoZV+by+MutzLIP5A16TRi/mS3m5AAAAAElFTkS + uQmCC + """) + + self.folder_icn = tk.PhotoImage("foldericon", data=""" + iVBORw0KGgoAAAANSUhEUgAAABAAAAAMCAYAAABr5z2BAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB + 3d3cuaW5rc2NhcGUub3Jnm+48GgAAAJBJREFUKJHdzTEKwkAUhOF/loCFRbAVr+IhLAWLCPaW3sFGPIOm1Bt4hxSSEwRs7Z + UdayErmnROO++bp93htJK0BUa8pxEq1ovZhQ/R/ni+G/LWEjW2y4Stx4NnmUU7l9R6YTxBbFLfb49sGlL4m9ieh84aAA17D + sCfDLiHdwDqrlpwDTHGAqiA+IONQIW0fAFkySdEGFdeCgAAAABJRU5ErkJggg==""") diff --git a/cupcake/diffeditor/__init__.py b/cupcake/diffeditor/__init__.py new file mode 100644 index 0000000..44ec92a --- /dev/null +++ b/cupcake/diffeditor/__init__.py @@ -0,0 +1,143 @@ +import re +import threading +import tkinter as tk + +from .pane import DiffPane +from .differ import Differ +from ..editor import BaseEditor + + +class DiffEditor(BaseEditor): + def __init__(self, master, path1, path2, language=None, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.config(bg=self.base.theme.border) + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(0, weight=1) + self.path1 = path1 + self.path2 = path2 + self.editable = True + + self.lhs_data = [] + self.rhs_data = [] + + self.lhs_last_line = 0 + self.rhs_last_line = 0 + + self.lhs = DiffPane(self, path1, language=language) + self.lhs.grid(row=0, column=0, sticky=tk.NSEW, padx=(0, 1)) + + self.rhs = DiffPane(self, path2, language=language) + self.rhs.grid(row=0, column=1, sticky=tk.NSEW) + + self.left = self.lhs.text + self.right = self.text = self.rhs.text + + self.lhs.scrollbar['command'] = self.on_scrollbar + self.rhs.scrollbar['command'] = self.on_scrollbar + self.left['yscrollcommand'] = self.on_textscroll + self.right['yscrollcommand'] = self.on_textscroll + + self.stipple = self.base.settings.stipple + + self.left.tag_config("addition", background=self.base.theme.diffeditor.notexist, bgstipple=f"@{self.stipple}") + self.left.tag_config("removal", background=self.base.theme.diffeditor.removed) + self.left.tag_config("removedword", background=self.base.theme.diffeditor.removedword) + + self.right.tag_config("addition", background=self.base.theme.diffeditor.added) + self.right.tag_config("removal", background=self.base.theme.diffeditor.notexist, bgstipple=f"@{self.stipple}") + self.right.tag_config("addedword", background=self.base.theme.diffeditor.addedword) + + self.differ = Differ(self) + + if path1 and path2: + with open(self.path1, 'r') as f: + self.lhs_data = f.read() + with open(self.path2, 'r') as f: + self.rhs_data = f.read() + + def on_scrollbar(self, *args): + self.left.yview(*args) + self.lhs.on_scroll() + + self.right.yview(*args) + self.rhs.on_scroll() + + def on_textscroll(self, *args): + self.lhs.scrollbar.set(*args) + self.rhs.scrollbar.set(*args) + self.on_scrollbar('moveto', args[0]) + + def run_show_diff(self): + threading.Thread(target=self.show_diff).start() + + def show_diff_text(self, lhs, rhs): + self.lhs_data = lhs + self.rhs_data = rhs + + self.show_diff() + + def show_diff(self): + self.left.set_active(True) + self.lhs.clear() + self.rhs.clear() + + lhs_lines = [line+"\n" for line in self.lhs_data.split('\n')] + rhs_lines = [line+"\n" for line in self.rhs_data.split('\n')] + + self.diff = list(self.differ.get_diff(lhs_lines, rhs_lines)) + for i, line in enumerate(self.diff): + marker = line[0] + content = line[2:] + + match marker: + case " ": + # line is same in both + self.left.write(content) + self.right.write(content) + + case "-": + # line is only on the left + self.left.write(content, "removal") + self.left.newline("addition") + + case "+": + # line is only on the right + self.right.write(content, "addition") + self.left.newline("addition") + + # case "?": + # # the above line has changes + # if matches := re.finditer(r'\++', content): + # self.left.delete(str(float(self.rhs_last_line+1)), str(float(int(float(self.left.index(tk.INSERT)))))) + # for match in matches: + # start = f"{self.rhs_last_line}.{match.start()}" + # end = f"{self.rhs_last_line}.{match.end()}" + # self.right.tag_add("addedword", start, end) + + # if matches := re.finditer(r'-+', content): + # self.right.delete(str(float(self.lhs_last_line+1)), str(float(int(float(self.right.index(tk.INSERT)))))) + # for match in matches: + # start = f"{self.lhs_last_line}.{match.start()}" + # end = f"{self.lhs_last_line}.{match.end()}" + # self.left.tag_add("removedword", start, end) + + self.left.update() + self.right.update() + + self.left.highlighter.highlight() + self.right.highlighter.highlight() + + # Add extra empty lines at the bottom if one side has fewer lines + lhs_line_count = int(float(self.left.index(tk.END))) - 1 + rhs_line_count = int(float(self.right.index(tk.END))) - 1 + if lhs_line_count > rhs_line_count: + extra_newlines = lhs_line_count - rhs_line_count + for _ in range(extra_newlines): + self.right.newline() + elif rhs_line_count > lhs_line_count: + extra_newlines = rhs_line_count - lhs_line_count + for _ in range(extra_newlines): + self.left.newline() + + self.left.set_active(False) diff --git a/cupcake/diffeditor/differ.py b/cupcake/diffeditor/differ.py new file mode 100644 index 0000000..6d6ceca --- /dev/null +++ b/cupcake/diffeditor/differ.py @@ -0,0 +1,11 @@ +import difflib + + +class Differ(difflib.Differ): + def __init__(self, master, *args, **kwargs): + super().__init__(*args, **kwargs) + self.master = master + self.base = master.base + + def get_diff(self, lhs, rhs): + return self.compare(lhs, rhs) diff --git a/cupcake/diffeditor/pane.py b/cupcake/diffeditor/pane.py new file mode 100644 index 0000000..f581f72 --- /dev/null +++ b/cupcake/diffeditor/pane.py @@ -0,0 +1,12 @@ +from ..texteditor import TextEditor + +# TODO currently using TextEditor, use Editor instead +class DiffPane(TextEditor): + def __init__(self, master, *args, **kwargs): + super().__init__(master, minimalist=True, *args, **kwargs) + + def load_file(self): + self.text.load_file() + + def load_text(self, text): + self.text.clear_insert(text) diff --git a/cupcake/editor.py b/cupcake/editor.py new file mode 100644 index 0000000..4f028cb --- /dev/null +++ b/cupcake/editor.py @@ -0,0 +1,21 @@ +from .utils import Frame + + +class BaseEditor(Frame): + """ + Base class for editors. + """ + def __init__(self, master, path=None, path2=None, editable=True, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.config(bg=self.base.theme.background) + + self.path = path + self.path2 = path2 + self.editable = editable + + self.showpath = False + self.content = None + self.diff = False + + def save(self, *_): + ... diff --git a/cupcake/editor/__init__.py b/cupcake/editor/__init__.py deleted file mode 100644 index 1b09f1e..0000000 --- a/cupcake/editor/__init__.py +++ /dev/null @@ -1,81 +0,0 @@ -import tkinter as tk - -from ..config import Config -from .events import Events -from .linenumbers import LineNumbers -from .minimap import Minimap -from .scrollbar import Scrollbar -from .language import SyntaxLoader -from .text import Text -from .find_replace import FinderReplacer - - -class Editor(tk.Frame): - def __init__(self, master, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.config = Config(self) - self.font = self.config.font - - self.syntax = SyntaxLoader() - - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(1, weight=1) - - self.text = Text(self) - self.linenumebers = LineNumbers(self, self.text) - self.minimap = Minimap(self, self.text) - self.scrollbar = Scrollbar(self, self.text) - - # actual find-replace widget - # self.find_replace = FindReplace(self, self.text) - # self.find_replace_active = False - self.find_replace = FinderReplacer(self) - self.find_replace.on_close() - #self.text.bind("", self.find_replace.revive) - - self.linenumebers.grid(row=0, column=0, sticky=tk.NS) - self.text.grid(row=0, column=1, sticky=tk.NSEW) - self.minimap.grid(row=0, column=2, sticky=tk.NS) - self.scrollbar.grid(row=0, column=3, sticky=tk.NS) - - self.events = Events(self) - self.text.config(yscrollcommand=self.text_scrolled) - self.focus() - - def text_scrolled(self, *args): - pass - - def show_find_replace(self, event): - # positioning of the actual find_replace widget - # if not self.find_replace_active: - # pos_x, pos_y, width = self.text.winfo_rootx(), self.text.winfo_rooty(), self.text.winfo_width() - # self.find_replace.show(((pos_x + width) - (self.find_replace.winfo_width() + 10), pos_y)) - # else: - # self.find_replace.reset() - self.find_replace.revive(event) - - def focus(self): - self.text.focus() - self.refresh_editor() - - def set_fontsize(self, size): - self.font.configure(size=size) - self.linenumebers.set_bar_width(size * 4) - - def refresh_editor(self, *_): - self.text.on_change() - self.text.highlighter.highlight_all() - self.redraw_ln() - self.minimap.redraw() - self.scrollbar.redraw() - - def redraw_ln(self, *_): - self.linenumebers.redraw() - - def insert(self, text): - self.text.clear_insert(text) - - def load_file(self, filepath): - self.text.load_file(filepath) diff --git a/cupcake/editor/autocomplete/item.py b/cupcake/editor/autocomplete/item.py deleted file mode 100644 index e361049..0000000 --- a/cupcake/editor/autocomplete/item.py +++ /dev/null @@ -1,119 +0,0 @@ -from cgitb import text -import tkinter as tk - -from .itemkinds import Kinds - - -class Kind(tk.Label): - def __init__(self, master, kinds, kind="text", *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - self.kinds = kinds - self.kind = kind - - self.image = None - - self.config_appearance() - self.config_image() - - def config_appearance(self): - self.config(bg="#252526") - - def config_image(self): - match self.kind: - case "method": - self.image = self.kinds.imethods - case "variable": - self.image = self.kinds.ivariables - case "field": - self.image = self.kinds.ifields - case "class": - self.image = self.kinds.iclasses - case "interface": - self.image = self.kinds.iinterfaces - case "module": - self.image = self.kinds.imodules - case "property": - self.image = self.kinds.iproperties - case "keyword": - self.image = self.kinds.ikeywords - case _: - self.image = self.kinds.iwords - self.config(image=self.image) - -class AutoCompleteItem(tk.Frame): - def __init__(self, master, left, kind=None, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.left = left - self.kind = kind - - self.kindw = Kind(self, self.master.autocomplete_kinds, kind) - self.leftw = tk.Text(self, - font=(self.master.font['family'], 10), fg="#d4d4d4", - bg="#252526", relief=tk.FLAT, highlightthickness=0, width=30, height=1) - self.leftw.insert(tk.END, left) - self.leftw.config(state=tk.DISABLED) - - self.leftw.tag_config("term", foreground="#18a3ff") - - self.config(bg="#1e1e1e", width=300) - - self.kindw.bind("", self.on_click) - self.leftw.bind("", self.on_click) - - self.bind("", self.on_hover) - self.bind("", self.off_hover) - - self.selected = False - self.hovered = False - - self.grid_columnconfigure(1, weight=1) - self.grid_rowconfigure(0, weight=1) - - self.kindw.grid(row=0, column=0, sticky=tk.NSEW) - self.leftw.grid(row=0, column=1, sticky=tk.NSEW) - - def get_text(self): - return self.left - - def get_kind(self): - return self.kind - - def mark_term(self, term): - start_pos = self.left.find(term) - end_pos = start_pos + len(term) - self.leftw.tag_remove("term", 1.0, tk.END) - self.leftw.tag_add("term", f"1.{start_pos}", f"1.{end_pos}") - - def on_click(self, *args): - self.master.choose(self) - - def on_hover(self, *args): - if not self.selected: - self.kindw.config(bg="#2a2d2e") - self.leftw.config(bg="#2a2d2e") - self.hovered = True - - def off_hover(self, *args): - if not self.selected: - self.kindw.config(bg="#252526") - self.leftw.config(bg="#252526") - self.hovered = False - - def toggle_selection(self): - if self.selected: - self.select() - else: - self.deselect() - - def select(self): - self.kindw.config(bg="#094771") - self.leftw.config(bg="#094771", fg="#ffffff") - self.selected = True - - def deselect(self): - self.kindw.config(bg="#252526") - self.leftw.config(bg="#252526", fg="#d4d4d4") - self.selected = False \ No newline at end of file diff --git a/cupcake/editor/events.py b/cupcake/editor/events.py deleted file mode 100644 index 626d77c..0000000 --- a/cupcake/editor/events.py +++ /dev/null @@ -1,13 +0,0 @@ -class Events: - def __init__(self, master, *args, **kwargs): - self.master = master - - self.bind_all() - - def bind(self, key, fn): - self.master.text.bind(key, fn) - - def bind_all(self): - self.bind("<>", self.master.refresh_editor) - self.bind("", self.master.redraw_ln) - self.bind("", self.master.show_find_replace) diff --git a/cupcake/editor/find_replace/__init__.py b/cupcake/editor/find_replace/__init__.py deleted file mode 100644 index 0af3ec4..0000000 --- a/cupcake/editor/find_replace/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -import tkinter as tk - -from .entrybox import EntryBox -from .button import Button -from .findbox import FindBox -from .replacebox import ReplaceBox -from .results import FindResults -from .togglew import ToggleWidget -from .container import FindReplaceContainer - -from .find_replace import FinderReplacer - - -class FindReplace(tk.Toplevel): - def __init__(self, master, tw, state=False, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - self.tw = tw - - self.state = state - self.font = self.master.font - - if not state: - self.withdraw() - - self.overrideredirect(True) - self.config(bg="#454545") - - self.togglew = ToggleWidget(self) - self.container = FindReplaceContainer(self) - - self.rowconfigure(0, weight=1) - self.grid_columnconfigure(1, weight=1) - - self.togglew.grid(row=0, column=0, sticky=tk.NS, padx=(2, 0)) - self.container.grid(row=0, column=1, sticky=tk.NSEW) - - self.config_bindings() - - def config_bindings(self, *args): - self.container.find_entry.entry.bind("", self.do_find) - - def toggle_replace(self, state): - self.container.toggle_replace(state) - - def do_find(self, *args): - print(self.container.get_term()) - # self.master.highlighter.highlight_pattern(self.container.find_entry.entry.get()) - - def refresh_geometry(self, *args): - self.update_idletasks() - self.geometry("+{}+{}".format(*self.master.cursor_screen_location())) - - def show(self, pos): - self.state = True - self.update_idletasks() - self.geometry("+{}+{}".format(*pos)) - self.deiconify() - self.master.find_replace_active = True - - def hide(self, *args): - self.state = False - self.withdraw() - self.master.find_replace_active = False - - def reset(self, *args): - ... - diff --git a/cupcake/editor/find_replace/button.py b/cupcake/editor/find_replace/button.py deleted file mode 100644 index a61ed19..0000000 --- a/cupcake/editor/find_replace/button.py +++ /dev/null @@ -1,41 +0,0 @@ -import tkinter as tk - - -class Button(tk.Frame): - def __init__(self, master, bg, hbg, img=None, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - self.img = img - self.hovered = False - - self.hbg = hbg - self.bg = bg - - self.imagew = tk.Label(self, image=self.img) - self.imagew.config(bg=self.bg, relief=tk.FLAT) - self.imagew.grid(row=0, column=0, sticky=tk.NS) - - self.config(bg=self.bg, pady=3, padx=3, cursor="hand2") - self.config_bindings() - - def config_bindings(self): - self.bind("", self.on_hover) - self.bind("", self.off_hover) - self.bind("", self.on_click) - - def set_image(self, img): - self.img = img - self.imagew.config(image=self.img) - - def on_click(self, *args): - pass - - def on_hover(self, *args): - self.hovered = True - self.imagew.config(bg=self.hbg) - self.config(bg=self.hbg) - - def off_hover(self, *args): - self.hovered = False - self.imagew.config(bg=self.bg) - self.config(bg=self.bg) diff --git a/cupcake/editor/find_replace/container.py b/cupcake/editor/find_replace/container.py deleted file mode 100644 index 468e769..0000000 --- a/cupcake/editor/find_replace/container.py +++ /dev/null @@ -1,68 +0,0 @@ -import tkinter as tk - -from . import Button -from . import FindResults -from . import FindBox -from . import ReplaceBox - - -class FindReplaceContainer(tk.Frame): - def __init__(self, master, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.config(bg="#252526") - self.replace_enabled = False - - self.find_entry = FindBox(self, width=20) - self.replace_entry = ReplaceBox(self, width=20) - - self.find_results = FindResults(self) - - self.replace_btn_holder = tk.Frame(self, bg="#252526", pady=2) - self.replace_button = Button(self.replace_btn_holder, bg="#252526", hbg="#4b4c4d", img=tk.PhotoImage(data=""" - iVBORw0KGgoAAAANSUhEUgAAABAAAAARCAYAAADUryzEAAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2Nhc - GUub3Jnm+48GgAAAjBJREFUOI2VUj1olEEQfbO7lwODEAjeCSoSIaAcWJgIosct+4mCYGORNoUWIqTRQsTKLqBYp1Dwp/xEJAFDwMC3+yE2Br - SxEUEsEiWJEDgTyG2+GQsvkjsTo6/bZd6892YG+A947xe7/8zfCCGE0wDOFUXxPkmSSQA9IYQrAKK19ikA0GZxnuf7iqIYNcYsMXNRFMU3pVQ - PgBUAz2OMx0ql0gKAMQA3ROSac+71bwfMPEVEg8y8CGBNa/2WmfcqpZoiYsrl8h5mjtbaJ977swD2d0c4Yoy5XK/Xp7ZkzkXkE4BYFIUQ0VoI - YZaIDiulrgOA8d4/IqIZEWnFGAe8969E5JJz7oeIJK1Wq9zf398aHh6O09PTA319fWp9fT02Go0NACDv/QUAzwAsAKiKyAQzP9Ba39xtK0T0k - gAgy7KLSqlJAPestbeyLDsK4OA/NHhIaZrqarVqY4yHtNbzu5FE5GuSJB/aM/piKpXKYxHRxpjmbuS2aiOEcNVamwO/tnCeiE5Zaz8DQJqmul - ar6Vqt1morKu+9cs5ttFVTZj4OIAcAtbV7CGGwUqm8WF5efhNCGM2yrB5CeKeUmk3TVG/nqOOUe3t755vNZg7AATgDoEFE9zfPdjt0OFhdXR0 - hohMAAgAopZYAnPTej8zNzZW24ceOBsw8KSIfieg7M0+Vy+VxEVkQkQNDQ0Mb3WwRGeuI4JxbAXCnq258J/vOuRlDRHdFxHvv/1DYARHA7c3H - T51aBFm+qi3JAAAAAElFTkSuQmCC""")) - - self.find_btns_holder = tk.Frame(self, bg="#252526", pady=6) - self.selection_button = Button(self.find_btns_holder, bg="#252526", hbg="#4b4c4d", img=tk.PhotoImage(data=""" - iVBORw0KGgoAAAANSUhEUgAAABEAAAALCAYAAACZIGYHAAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2Nhc - GUub3Jnm+48GgAAAGdJREFUKJFjPHnx5n8GEsF/RgZvCz31bTA+yz8GJmNSDWH8xnCHVD30AYynLt+RpdQQlv///j6i2BAGhv9KlBoyeADjkX - M3pIhRyPGP8ZeJifobbHIsrMyMT4kx5A8zwykGBgZzbHIAsMwZm/JJgOgAAAAASUVORK5CYII=""")) - self.close_button = Button(self.find_btns_holder, bg="#252526", hbg="#4b4c4d", img=tk.PhotoImage(data=""" - iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2Nhc - GUub3Jnm+48GgAAALhJREFUGJV1kL0OAVEQhb9ze61HkK2sxk8k3sqqUNhEgafS2Evjqna9hFKiMAok3OxOOfOdM3NGPlwz8WwP0iSjoYpzmT - t4OBMtQ9NjKLdNoKS5ida7cbmufKgsFhTnMvehMh/Kzb9DJKgDFQtktjB7FpIbgW2HaTKrhQF8qPbABLPTsJcMfmcuvhGYgB2Q+nEG9wtKmn9 - Wj03Ka7/UlDoOrchxRlTf0MJ2TtIdWNeBAKNuZwmsDd1eZYKFNbEaSb4AAAAASUVORK5CYII=""")) - - self.grid_columnconfigure(0, weight=1) - - self.find_entry.grid(row=0, column=0, sticky=tk.NSEW, pady=5) - - self.find_results.grid(row=0, column=1, sticky=tk.NSEW, pady=5) - self.replace_button.grid(row=0, column=0, sticky=tk.NS, padx=5) - - self.find_btns_holder.grid(row=0, column=2, sticky=tk.NSEW, padx=(10, 5)) - - self.selection_button.grid(row=0, column=0, sticky=tk.NSEW, pady=3) - self.close_button.grid(row=0, column=1, sticky=tk.NSEW, pady=3) - - def get_term(self): - return self.find_entry.get() - - def toggle_replace(self, state): - if self.replace_enabled: - self.replace_enabled = False - self.replace_entry.grid_remove() - self.replace_btn_holder.grid_remove() - else: - self.replace_enabled = True - self.replace_entry.grid(row=1, column=0, sticky=tk.NSEW, pady=(0, 5)) - self.replace_btn_holder.grid(row=1, column=1, sticky=tk.NSEW, pady=(0, 5)) diff --git a/cupcake/editor/find_replace/entrybox.py b/cupcake/editor/find_replace/entrybox.py deleted file mode 100644 index e08ecc8..0000000 --- a/cupcake/editor/find_replace/entrybox.py +++ /dev/null @@ -1,39 +0,0 @@ -import tkinter as tk -from tkinter import font - - -class EntryBox(tk.Frame): - def __init__(self, master, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.config(bg="#3c3c3c", padx=1, pady=1) - self.grid_columnconfigure(0, weight=1) - self.grid_rowconfigure(0, weight=1) - - self.term = tk.StringVar() - - self.entry_frame = frame = tk.Frame(self, bg="#3c3c3c") - frame.grid(row=0, column=0, sticky=tk.NSEW) - - self.entry = tk.Entry( - frame, width=30, bg="#3c3c3c", fg="#ccccc7", font=("Helvetica", 11), - textvariable=self.term, relief=tk.FLAT, insertbackground="#aeafad") - self.entry.grid(sticky=tk.EW, padx=3, pady=3) - - self.config_bindings() - - def get(self): - return self.term.get() - - def config_bindings(self, *args): ... - # self.entry.bind("", self.on_focus) - # self.entry.bind("", self.off_focus) - - def on_focus(self, *args): - self.update_idletasks() - self.config(bg="#007fd4") - - def off_focus(self, *args): - self.update_idletasks() - self.config(bg="#3c3c3c") \ No newline at end of file diff --git a/cupcake/editor/find_replace/find_replace.py b/cupcake/editor/find_replace/find_replace.py deleted file mode 100644 index b3901ec..0000000 --- a/cupcake/editor/find_replace/find_replace.py +++ /dev/null @@ -1,226 +0,0 @@ -import tkinter as tk -import re - - -class FinderReplacer: - """A class to hold all the find/replace functionality - it will have the following attributes: - - matchstring - the string that the user wants to find. - replacestring - the string that the user wants to replace, - if the user just wants to search is None. - matches - a dict of matches with position as key and match - object as value. - current - the current position the user is interacting with - """ - - def __init__(self, parent, matchstring=None, replacestring=None): - self.matchstring = matchstring - self.replacestring = replacestring - self.matches = None - self.parent = parent - self.parent.text.tag_configure("found", background="green") - self.parent.text.tag_configure( - "foundcurrent", background="orange") - self.display() - - @property - def text(self): - return self.parent.text.get(1.0, tk.END) - - @property - def current(self): - if not self.parent.text.count("1.0", self.parent.text.index(tk.INSERT), "chars"): - return 0 - else: - return self.parent.text.count("1.0", self.parent.text.index(tk.INSERT), "chars")[0] - - def display(self): - self.window = tk.Toplevel(self.parent) - self.window.geometry("500x300") - self.window.title("find & replace") - self.window_ROWS = 5 - self.window_COLS = 3 - - for i in range(self.window_ROWS): - self.window.rowconfigure(i, minsize=35) - for i in range(self.window_COLS): - self.window.columnconfigure(i, minsize=30) - self.find_tag = tk.Label(self.window, text="Find: ") - self.find_entry = tk.Entry(self.window) - self.replace_tag = tk.Label(self.window, text="Replace for:") - self.replace_entry = tk.Entry(self.window) - self.find_tag.grid(row=1, column=1) - self.find_entry.grid(row=1, column=2) - self.replace_tag.grid(row=2, column=1) - self.replace_entry.grid(row=2, column=2) - self.find_button = tk.Button( - self.window, text="Highlight", command=self.find) - self.find_button.grid(row=1, column=4) - self.next_button = tk.Button( - self.window, text="->", command=self.next_match) - self.next_button.grid(row=1, column=5) - self.prev_button = tk.Button( - self.window, text="<-", command=self.prev_match) - self.prev_button.grid(row=1, column=3) - self.replace_button = tk.Button( - self.window, text="Change it!", command=self.replace) - self.replace_all_button = tk.Button( - self.window, text="ALL", command=self.replace_all) - self.replace_button.grid(row=2, column=4) - self.replace_all_button.grid(row=2, column=5) - self.window.protocol("WM_DELETE_WINDOW", self.on_close) - - def highlight_matches(self): - self.parent.text.tag_remove("found", "1.0", "end") - self.parent.text.tag_remove("foundcurrent", "1.0", "end") - for pos, match in self.matches.items(): - start = match.start() - end = match.end() - self.parent.text.tag_add( - "found", f"1.0+{start}c", f"1.0+{end}c") - if self.is_on_match(): - self.highlight_current() - - def highlight_current(self): - self.parent.text.tag_remove("foundcurrent", "1.0", "end") - current = self.current - match = self.matches[current] - start = match.start() - end = match.end() - self.parent.text.tag_add( - "foundcurrent", f"1.0+{start}c", f"1.0+{end}c") - - def get_find_input(self): - if self.find_entry.get() == "": - self.parent.text.tag_remove("found", "1.0", "end") - self.parent.text.tag_remove("foundcurrent", "1.0", "end") - return - current = self.current - self.matches = {} - self.matchstring = self.find_entry.get() - self.re_ = re.compile(self.matchstring) - for match in self.re_.finditer(self.text): - self.matches[match.start()] = match - self.highlight_matches() - self.parent.text.mark_set("insert", f"1.0 + {current}c") - - def find(self): - self.get_find_input() - self.parent.lift() - self.parent.text.focus() - - def next_match(self): - """Moves the editor focus to the next match""" - if self.find_entry.get() != self.matchstring: - self.get_find_input() - matchpos = [i for i in sorted(self.matches.keys()) if i > self.current] - if len(matchpos) > 0: - next_index = f"1.0 + {matchpos[0]}c" - self.parent.text.mark_set("insert", next_index) - self.parent.text.see(next_index) - self.highlight_current() - elif len(self.matches) > 0: - self.parent.text.mark_set("insert", "1.0") - if self.is_on_match(): - self.highlight_current() - else: - self.next_match() - self.parent.lift() - self.parent.text.focus() - - def prev_match(self): - """Moves the editor focus to the previous match""" - if self.find_entry.get() != self.matchstring: - self.get_find_input() - matchpos = [i for i in sorted(self.matches.keys()) if i < self.current] - if len(matchpos) > 0: - next_index = f"1.0 + {matchpos[-1]}c" - self.parent.text.mark_set("insert", next_index) - self.parent.text.see(next_index) - self.highlight_current() - elif len(self.matches) > 0: - self.parent.text.mark_set("insert", "end") - self.prev_match() - self.parent.lift() - self.parent.text.focus() - - def replace(self): - """replaces current (in focus) match, removing the match and writing the replace string""" - self.replacestring = self.replace_entry.get() - if self.find_entry.get() != self.matchstring: - self.get_find_input() - if self.is_on_match(): - match = self.matches[self.current] - self.parent.text.delete( - f"1.0 + {match.start()}c", f"1.0 + {match.end()}c") - self.parent.text.insert( - f"1.0 + {self.current}c", self.replacestring) - self.get_find_input() - self.parent.lift() - self.parent.text.focus() - - def is_on_match(self): - """tells if the editor is currently pointing to a match""" - if self.current in self.matches.keys(): - return True - else: - return False - - def on_close(self): - """removes the highlighting of the find string when the window is closed""" - self.parent.text.tag_remove("found", "1.0", "end") - self.parent.text.tag_remove("foundcurrent", "1.0", "end") - self.window.withdraw() - - def replace_all(self): - """replaces all occurences of the string for the replace string, it will even replace partial words.""" - self.get_find_input() - nmatches = len(self.matches) - current = self.current - self.parent.text.mark_set("insert", "1.0") - self.replace() - for i in range(nmatches): - self.next_match() - self.replace() - self.parent.text.mark_set("insert", f"1.0 + {current}c") - - def revive(self, event): - """brings the window back""" - if self.parent.text.tag_ranges(tk.SEL): - selection = self.parent.text.get(tk.SEL_FIRST, tk.SEL_LAST) - self.find_entry.delete("0", "end") - self.find_entry.insert("0", selection) - self.parent.text.mark_set("insert", tk.SEL_FIRST) - self.get_find_input() - - self.window.deiconify() - self.window.lift() - self.find_entry.focus() - - -if __name__ == '__main__': - - class EditorMock(tk.Tk): - def __init__(self, text=""): - super().__init__() - self.text = tk.Text(self) - self.text.textw = self.text - self.text.pack() - self.text.insert(tk.END, text) - self.findr = FinderReplacer(self) - self.bind("", self.findr.revive) - self.mainloop() - - - - e = EditorMock(text="""EMACS: The Extensible, Customizable Display Editor -You are reading about GNU Emacs, the GNU incarnation of the advanced, self-documenting, customizable, extensible editor Emacs. (The โ€˜Gโ€™ in GNU (GNUโ€™s Not Unix) is not silent.) - -We call Emacs advanced because it can do much more than simple insertion and deletion of text. It can control subprocesses, indent programs automatically, show multiple files at once, edit remote files like they were local files, and more. Emacs editing commands operate in terms of characters, words, lines, sentences, paragraphs, and pages, as well as expressions and comments in various programming languages. - -Self-documenting means that at any time you can use special commands, known as help commands, to find out what your options are, or to find out what any command does, or to find all the commands that pertain to a given topic. See Help. - -Customizable means that you can easily alter the behavior of Emacs commands in simple ways. For instance, if you use a programming language in which comments start with โ€˜<**โ€™ and end with โ€˜**>โ€™, you can tell the Emacs comment manipulation commands to use those strings (see Manipulating Comments). To take another example, you can rebind the basic cursor motion commands (up, down, left and right) to any keys on the keyboard that you find comfortable. See Customization. - -Extensible means that you can go beyond simple customization and create entirely new commands. New commands are simply programs written in the Lisp language, which are run by Emacsโ€™s own Lisp interpreter. Existing commands can even be redefined in the middle of an editing session, without having to restart Emacs. Most of the editing commands in Emacs are written in Lisp; the few exceptions could have been written in Lisp but use C instead for efficiency. Writing an extension is programming, but non-programmers can use it afterwards. See Preface in An Introduction to Programming in Emacs Lisp, if you want to learn Emacs Lisp programming.""") diff --git a/cupcake/editor/find_replace/findbox.py b/cupcake/editor/find_replace/findbox.py deleted file mode 100644 index 822a720..0000000 --- a/cupcake/editor/find_replace/findbox.py +++ /dev/null @@ -1,34 +0,0 @@ -import tkinter as tk - -from . import Button -from . import EntryBox - - -class FindBox(EntryBox): - def __init__(self, master, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.btn_frame = tk.Frame(self, bg="#3c3c3c") - self.btn_frame.grid(row=0, column=1, sticky=tk.NSEW) - - self.full_word = Button(self.btn_frame, bg="#3c3c3c", hbg="#4b4c4d", img=tk.PhotoImage(data=""" - iVBORw0KGgoAAAANSUhEUgAAABMAAAANCAYAAABLjFUnAAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHR - Tb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAahJREFUKJGdkrFrU1EYxX/fS4JGdKkOLo5NI9jYak - wqpA7OxaHi5uDSQXASOgniHyAdXERBKNV/wKEuLiKIvc8MuemS9wSpUARByJBBjH33uLQhtiQGf+s999xzv - vvBhDifrDufPBqniSY1m4T8odcXDV2QrEjEl1y/t1mtVn8Paz61Ps9nll0zs6+12dJrM9OoZE8EDTM7h3iY - FU6+lWRD53eCacOghngWb6ePhy8bI2g2v53ICr1uCGHm6vz5HeeTdWCvVimtmJncdqdKsA99+3V2sVLpHq3 - ZSpdkum3icma9KaBAlJ8CdvYluwe16rPlpvPJz+Ph2DQQ/1XT+c51TK8Qm8oVGvWLM6eB7qjk+8WCxGBmg2 - TC6obeL8yVXwJIiuJ2OvK3m+2knEnFflGdo2YhfLQoeuB8uoppL24nt8BOHfK44XyyC5CJ+8Bao1zuDcy2f - PI8p+jplbnpd86nK0a4KfgRyN0zhSURvgOY9AazWNgl4AxorVYpvYCDdQl3cT5R3EqXx89mPHErXXY+UR5A - xsKWT0euyb8Q1AHyBhJa/W+nIf4AQku6KGfDUgAAAAAASUVORK5CYII=""")) - self.regex_button = Button(self.btn_frame, bg="#3c3c3c", hbg="#4b4c4d", img=tk.PhotoImage(data=""" - iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHRTb2Z0 - d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAPJJREFUKJGtkL1KwwAUhb+bFK0SCp18g4o4VBGsi4+ig7joC/ga - Qp9AN/sSDg4m6WB+DDYNiJu4KS5qaY6DiJXETp7tXr5zDhyYo2FUbAZx/jz7a9SBfjQ6WLL3iw/N/NL7FTTZciqw - X7TM7OiN5q1K7QKEcX5s5WSE2Le6BklOkBaHVnKKoybwhDjpdVfPKw0AUfTQQuW6mVxAQstC7cFAbqXhKknaC1q8 - AzLX3P6U6Vkp7TnQB0tq1/HjYgcgvBlvBEn+AnCZZd4wyrdrDd+6TsdrYZz/SrUgzl9nzsdet9OZF9IAvB9e3t/o - l2pX+lfDJ85DYGsTUOfAAAAAAElFTkSuQmCC""")) - - self.full_word.grid(row=0, column=0, sticky=tk.NSEW, pady=2, padx=(3, 1)) - self.regex_button.grid(row=0, column=2, sticky=tk.NSEW, pady=2, padx=(1, 3)) diff --git a/cupcake/editor/find_replace/replacebox.py b/cupcake/editor/find_replace/replacebox.py deleted file mode 100644 index f88565c..0000000 --- a/cupcake/editor/find_replace/replacebox.py +++ /dev/null @@ -1,25 +0,0 @@ -import tkinter as tk - -from . import Button -from . import EntryBox - - -class ReplaceBox(EntryBox): - def __init__(self, master, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.btn_frame = tk.Frame(self, bg="#3c3c3c") - self.btn_frame.grid(row=0, column=1, sticky=tk.NSEW) - - self.keep_case = Button(self.btn_frame, bg="#3c3c3c", hbg="#4b4c4d", img=tk.PhotoImage(data=""" - iVBORw0KGgoAAAANSUhEUgAAABAAAAAKCAYAAAC9vt6cAAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHRTb2Z0d2FyZQB - 3d3cuaW5rc2NhcGUub3Jnm+48GgAAAYVJREFUKJFtkD1rVGEQhZ957xsEQ2ysRTBcNteQzUbQLIKFIJgfkGXzBwTBRv+Ajb - WgAW1E7MRixSIgaIpYuJrrLmY/JO4HWNkIEgu/MHrfY7O5RrLTzXDmmXPG2Fdpu78FlJS52fKpeHtv3ugMLgRpfdRmwGfQk - x9HDl1ze6LNre0YSICmRaHCuFJYUsgSw10CV538+nslB7jIV0HPDR4IVsbtG/5jeeHk8Mx8vGZSU9LxHCCpAlYLbuKxQfym - NZwd6wJodofTMhaEe+YAGu1eAZjZtV9r5bkTn4C67GAMWXYvbfXrWQhNoD7ld986gExWxVg/Vyx+GSlrjIth9tDQbeAWcPF - bFq16AGcsS8Rpe7AzuuWBqVed93Nni0k3/xP29HSp8AFgs9XzhrviX3cHiYISifO47Oc/v+6uI6oAOeB/M/YH9N27oKqJjc - VS4eV+QdrpP0K6DFzPmdixRrs3EbBF4KrgvseoSLZ68EJUk7Kb6bv+PGEEUHghDGAHdOfo4ejGX3P9oluH0boRAAAAAElFT - kSuQmCC""")) - - self.keep_case.grid(row=0, column=0, sticky=tk.NSEW, pady=2, padx=(3, 1)) diff --git a/cupcake/editor/find_replace/results.py b/cupcake/editor/find_replace/results.py deleted file mode 100644 index 19db7e6..0000000 --- a/cupcake/editor/find_replace/results.py +++ /dev/null @@ -1,21 +0,0 @@ -import tkinter as tk - - -class FindResults(tk.Label): - def __init__(self, master, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.init() - self.config(padx=1, width=10, font=("Helvetica", 10), bg="#252526") - - def show(self, n): - if not n: - self.config(text="No results") - self.config(fg="#f48771") - else: - self.config(text=f"{n} results") - - def init(self): - self.config(text="No results") - self.config(fg="#cccccc") \ No newline at end of file diff --git a/cupcake/editor/find_replace/toggle.py b/cupcake/editor/find_replace/toggle.py deleted file mode 100644 index 0763ed3..0000000 --- a/cupcake/editor/find_replace/toggle.py +++ /dev/null @@ -1,52 +0,0 @@ -import tkinter as tk - - -class ToggleButton(tk.Frame): - def __init__(self, master, bg, hbg, sbg, img=None, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - self.img = img - - self.hovered = False - self.state = False - - self.bg = bg - self.hbg = hbg - self.sbg = sbg - - self.imagew = tk.Label(self, image=self.img) - self.imagew.config(bg=self.bg, relief=tk.FLAT) - self.imagew.grid(row=0, column=0, sticky=tk.NS) - - self.config(bg=self.bg, pady=3, padx=3, cursor="hand2") - self.config_bindings() - - def config_bindings(self): - self.bind("", self.on_hover) - self.bind("", self.off_hover) - self.bind("", self.toggle) - - def set_image(self, img): - self.img = img - self.imagew.config(image=self.img) - - def redraw(self): - if self.state: - self.config(bg=self.sbg) - return - - if self.hovered: - self.config(bg=self.hbg) - else: - self.config(bg=self.bg) - - def toggle(self, *args): - self.state = not self.state - - def on_hover(self, *args): - self.hovered = True - self.redraw() - - def off_hover(self, *args): - self.hovered = False - self.redraw() diff --git a/cupcake/editor/find_replace/togglew.py b/cupcake/editor/find_replace/togglew.py deleted file mode 100644 index 78f6f67..0000000 --- a/cupcake/editor/find_replace/togglew.py +++ /dev/null @@ -1,61 +0,0 @@ -import tkinter as tk - - -class ToggleWidget(tk.Frame): - def __init__(self, master, img=None, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - self.img = img - - self.state = False - self.hovered = False - - self.hbg = "#363737" - self.bg = "#252526" - - self.image = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAAAcAAAANC - AYAAABlyXS1AAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHRTb2Z0d2FyZQB3d3cua - W5rc2NhcGUub3Jnm+48GgAAAHZJREFUGJWFkLENQjEQQ1+yCTOk/ENRUFDRIEBCHzEFb - EIXewr2yNFQIEg+1/rd2T4krSVd6EwGVsDG9rkHIOkkKUYXqLUeFwHbh08gdSz2wC6ld - PsR38ADmPJgcwLui57/0w572r5Kit6HcmvtGRFzKWX7Lb4AhaddqGaZ7iQAAAAASUVOR - K5CYII=""") - self.image_toggled = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAA - AwAAAAGCAYAAAD37n+BAAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHRTb2Z0d2FyZ - QB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAKxJREFUGJV9jCFOA1EURc+tYhVogv1/sIwtg - m4Bg2AD7QpIMAQMhmXUkCIwqM6/MwJDF1FDJkjyMDMJQwLXnnOubD9GxHtVVXf8M9tXk - o5nktaSrm3f/CWXUpbAPfAKQNM0p7Y/bD9ExOzX88r2ZyllDqARtG2bIuIJeO77/qKu6 - 6+u624j4lLSIqX0MgmGtyNgA7wBe+AcOMs5b0dnEgzRoSRHxAFwknPe/eTfdaJLHIQZk - jwAAAAASUVORK5CYII=""") - - self.imagew = tk.Label(self, image=self.image) - self.imagew.config(bg=self.bg, relief=tk.FLAT, fg="#c5c5c5") - self.imagew.grid(row=0, column=0, sticky=tk.NS) - - self.config(bg=self.bg, cursor="hand2", pady=10, padx=5) - self.config_bindings() - - def config_bindings(self): - self.bind("", self.on_hover) - self.bind("", self.off_hover) - self.bind("", self.toggle) - self.imagew.bind("", self.toggle) - - def toggle(self, *args): - self.state = not self.state - if self.state: - self.imagew.config(image=self.image_toggled) - self.config(pady=30, padx=2) - else: - self.imagew.config(image=self.image) - self.config(pady=10, padx=5) - self.master.toggle_replace(self.state) - - def on_hover(self, *args): - self.hovered = True - self.imagew.config(bg=self.hbg) - self.config(bg=self.hbg) - - def off_hover(self, *args): - self.hovered = False - self.imagew.config(bg=self.bg) - self.config(bg=self.bg) diff --git a/cupcake/editor/highlighter.py b/cupcake/editor/highlighter.py deleted file mode 100644 index 53ecba1..0000000 --- a/cupcake/editor/highlighter.py +++ /dev/null @@ -1,43 +0,0 @@ -import tkinter as tk - - -class Highlighter: - def __init__(self, master, *args, **kwargs): - self.text = master - - self.syntax = master.master.syntax - self.setup_highlight_tags() - - def setup_highlight_tags(self): - self.text.tag_configure("keywords", foreground="#559dd2") - self.text.tag_configure("strings", foreground="#cf8e7c") - self.text.tag_configure("numbers", foreground="#b5cfab") - self.text.tag_configure("comments", foreground="#699b5c") - - def highlight_pattern(self, pattern, tag, start="1.0", end=tk.END, regexp=False): - start = self.text.index(start) - end = self.text.index(end) - - self.text.mark_set("matchStart", start) - self.text.mark_set("matchEnd", start) - self.text.mark_set("searchLimit", end) - - self.text.tag_remove(tag, start, end) - - count = tk.IntVar() - while True: - index = self.text.search(pattern, "matchEnd", "searchLimit", count=count, regexp=regexp) - if index == "" or count.get() == 0: - break - - self.text.mark_set("matchStart", index) - self.text.mark_set("matchEnd", f"{index}+{count.get()}c") - - self.text.tag_add(tag, "matchStart", "matchEnd") - - def highlight_all(self): - self.highlight_pattern(self.syntax.rgx_keywords, "keywords", regexp=True) - self.highlight_pattern(self.syntax.rgx_numbers, "numbers", regexp=True) - - self.highlight_pattern(self.syntax.rgx_strings, "strings", regexp=True) - self.highlight_pattern(self.syntax.rgx_comments, "comments", regexp=True) diff --git a/cupcake/editor/language/__init__.py b/cupcake/editor/language/__init__.py deleted file mode 100644 index 76f74ec..0000000 --- a/cupcake/editor/language/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .syntax import SyntaxLoader diff --git a/cupcake/editor/language/languages/__init__.py b/cupcake/editor/language/languages/__init__.py deleted file mode 100644 index 9514657..0000000 --- a/cupcake/editor/language/languages/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -languages = { - 'plain': { - 'name': 'Plain Text', - 'extensions': ["txt"], - 'executable': False, - 'autocomplete': None, - }, - 'cpp': { - 'name': 'C++', - 'extensions': ["cpp", "c", "hpp", "h"], - 'executable': True, - 'autocomplete': None, - }, -} diff --git a/cupcake/editor/language/languages/cpp.py b/cupcake/editor/language/languages/cpp.py deleted file mode 100644 index c095434..0000000 --- a/cupcake/editor/language/languages/cpp.py +++ /dev/null @@ -1,5 +0,0 @@ -class CppLSP: - def __init__(self, master, *args, **kwargs): - self.master = master - - \ No newline at end of file diff --git a/cupcake/editor/language/syntax.py b/cupcake/editor/language/syntax.py deleted file mode 100644 index 2c41b2d..0000000 --- a/cupcake/editor/language/syntax.py +++ /dev/null @@ -1,24 +0,0 @@ -from ...config.languages import CPP - - -class SyntaxLoader: - def __init__(self): - self.syntax = CPP() - self.setup_tokens() - - def setup_tokens(self): - self.keywords = self.syntax.keywords - self.numbers = self.syntax.numbers - self.strings = self.syntax.strings - self.comments = self.syntax.comments - - self.regexize_tokens() - - def regexize_tokens(self): - self.rgx_keywords = "|".join([f"\\y{i}\\y" for i in self.keywords]) - self.rgx_numbers = "|".join(self.numbers) - self.rgx_strings = "|".join(self.strings) - self.rgx_comments = "|".join(self.comments) - - def get_autocomplete_list(self): - return [(i, "keyword") for i in self.keywords] diff --git a/cupcake/editor/language/test.py b/cupcake/editor/language/test.py deleted file mode 100644 index c800483..0000000 --- a/cupcake/editor/language/test.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -[LT] Language Tools for assistance in coding - -- Detection of programming language used through extensions -- Execution of scripts -- Semantic syntax highlighting (extra) -- Smart Code completions (extra) - -""" - -from ...config import languages - - -class LanguageTools: - def __init__(self, master, *args, **kwargs): - self.master = master - - def get_language(self, filename): - """ - Get the language of a file - """ - ext = filename.split('.')[-1] - if ext in languages: - return ext - else: - return 'plain' - - def get_language_name(self, language): - """ - Get the name of a language - """ - if language in languages: - return self.languages[language]['name'] - else: - return 'Plain Text' - - def get_language_extensions(self, language): - """ - Get the extensions of a language - """ - if language in languages: - return self.languages[language]['extensions'] - else: - return [] diff --git a/cupcake/editor/linenumbers/__init__.py b/cupcake/editor/linenumbers/__init__.py deleted file mode 100644 index 115e636..0000000 --- a/cupcake/editor/linenumbers/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -import tkinter as tk - -from .breakpoint import Breakpoint - - -class LineNumbers(tk.Frame): - def __init__(self, master, text, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.config_appearance() - - self.text = text - - self.cw = tk.Canvas(self) - self.cw.config(width=68, bg="#1e1e1e", highlightthickness=0) - self.cw.pack(fill=tk.BOTH, expand=True) - - def set_bar_width(self, width): - self.configure(width=width) - - def config_appearance(self): - self.font = self.master.font - self.fill = "#858585" - self.highlight_fill = "#c6c6c6" - self.config(bg="#1e1e1e") - - def attach(self, text): - self.text = text - - def clear(self): - self.cw.delete(tk.ALL) - - def mark_line(self, line): - dline = self.text.get_line_info(line) - - if not dline: - return - - y = dline[1] - btn = tk.Menubutton(self.cw, - text=">", font=("Consolas", 14), fg="#1e1e1e", bg="#1e1e1e", cursor="hand2", - activeforeground="#c5c5c5", activebackground="#1e1e1e", borderwidth=0, - width=2, height=1, pady=0, padx=0, relief=tk.FLAT) - self.cw.create_window(70, y-2, anchor=tk.NE, window=btn) - - def highlight_current_line(self): - self.mark_line(tk.INSERT) - - def select_line(self, line): - self.text.select_line(line) - - def redraw(self, *args): - self.clear() - self.highlight_current_line() - self.redraw_line_numbers() - - def redraw_line_numbers(self): - i = self.text.get_origin() - while True: - dline = self.text.get_line_info(i) - if not dline: - break - - y = dline[1] - ln = str(i).split(".")[0] - - curline = self.text.get_line_info(tk.INSERT) - cur_y = None - if curline: - cur_y = curline[1] - - if y == cur_y: - number = self.cw.create_text(46, y, anchor=tk.NE, text=ln, font=self.font, fill=self.highlight_fill, tag=i) - else: - number = self.cw.create_text(46, y, anchor=tk.NE, text=ln, font=self.font, fill=self.fill, tag=i) - - self.cw.tag_bind(i, "", lambda _, i=i: self.select_line(i)) - - # drawing breakpoints - needs optimisations - # self.draw_breakpoint(y) - - i = self.text.index(f"{i}+1line") - - def draw_breakpoint(self, y): - bp = Breakpoint(self.cw) - self.cw.create_window(21, y-2, anchor=tk.NE, window=bp) - - def toggle_breakpoint(self, y): - ... diff --git a/cupcake/editor/scrollbar.py b/cupcake/editor/scrollbar.py deleted file mode 100644 index f7d62c2..0000000 --- a/cupcake/editor/scrollbar.py +++ /dev/null @@ -1,67 +0,0 @@ -import tkinter as tk - - -class Scrollbar(tk.Frame): - def __init__(self, master, textw, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.tw = textw - self.font = ("Arial", 1, "bold") - - self.config(bg="#252526", highlightthickness=0, padx=1) - self.cw = tk.Canvas(self, bg="#1e1e1e", width=15, highlightthickness=0) - self.cw.pack(fill=tk.BOTH, expand=True, side=tk.LEFT) - - self.slider_image = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAAG4AAABFCAYAAACrMNMO - AAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAMBJRE - FUeJzt0UENwCAAwMAxLajjhwOkz8M+pMmdgiYda5/5kPPeDuAf46KMizIuyrgo46KMizIuyrgo46KMizIuyrgo - 46KMizIuyrgo46KMizIuyrgo46KMizIuyrgo46KMizIuyrgo46KMizIuyrgo46KMizIuyrgo46KMizIuyrgo46 - KMizIuyrgo46KMizIuyrgo46KMizIuyrgo46KMizIuyrgo46KMizIuyrgo46KMizIuyrgo46KMizIu6gNeAwIJ - 26ERewAAAABJRU5ErkJggg==""") - - self.cw.create_image(0, 0, image=self.slider_image, anchor=tk.NW, tag="slider") - - self.extra_y = 10 - self.y_top_lim = 0 - self.drag_data = {"y": 0, "item": None} - self.yvalue = 0 - - self.cw.tag_bind("slider", "", self.drag_start) - self.cw.tag_bind("slider", "", self.drag_stop) - self.cw.tag_bind("slider", "", self.drag) - - if textw: - self.redraw() - - def attach(self, textw): - self.tw = textw - - def redraw(self): - self.y_bottom_lim = int(self.tw.index(tk.END).split(".")[0]) * 2 + self.extra_y - - def drag_start(self, event): - self.drag_data["item"] = self.cw.find_closest(event.x, event.y)[0] - self.drag_data["y"] = event.y - - def drag_stop(self, event): - self.drag_data["item"] = None - self.drag_data["y"] = 0 - - def drag(self, event): - item = self.drag_data["item"] - if item != 1: - return - - delta_y = event.y - self.drag_data["y"] - self.cw.move(item, 0, delta_y) - self.drag_data["y"] = event.y - - self.yvalue = y = self.cw.coords(item)[1] - if y <= self.y_top_lim: - self.cw.move("slider", 0, -(y - self.y_top_lim)) - elif y >= self.y_bottom_lim: - self.cw.move("slider", 0, -(y - self.y_bottom_lim)) - - self.tw.yview(int(y / self.cw.winfo_height() * 100)) - self.tw.master.redraw_ln() diff --git a/cupcake/editor/text.py b/cupcake/editor/text.py deleted file mode 100644 index dfa65c9..0000000 --- a/cupcake/editor/text.py +++ /dev/null @@ -1,317 +0,0 @@ -import re -import tkinter as tk - -from .highlighter import Highlighter -from .autocomplete import AutoComplete -from .language import SyntaxLoader - - -class Text(tk.Text): - def __init__(self, master, *args, **kwargs): - super().__init__(master) - self.master = master - - self.font = self.master.font - self.syntax = self.master.syntax - self.pack_propagate(False) - - self.keywords = self.syntax.keywords - self.current_word = None - self.words = [] - - self.highlighter = Highlighter(self) - - self.current_indentation = None - self.current_line = None - - self.auto_completion = AutoComplete( - self, items=self.syntax.get_autocomplete_list()) - - self.create_proxy() - self.config_appearance() - self.config_tags() - self.config_bindings() - - def create_proxy(self): - self._orig = self._w + "_orig" - self.tk.call("rename", self._w, self._orig) - self.tk.createcommand(self._w, self._proxy) - - def _proxy(self, *args): - cmd = (self._orig,) + args - result = self.tk.call(cmd) - - if (args[0] in ("insert", "replace", "delete") or - args[0:3] == ("mark", "set", "insert") or - args[0:2] == ("xview", "moveto") or - args[0:2] == ("xview", "scroll") or - args[0:2] == ("yview", "moveto") or - args[0:2] == ("yview", "scroll") - ): - self.event_generate("<>", when="tail") - - return result - - def config_appearance(self): - self.config( - font=self.master.font, bg="#1e1e1e", - fg="#d4d4d4", wrap=tk.NONE, relief=tk.FLAT, - highlightthickness=0, insertbackground="#aeafad") - - def config_bindings(self): - self.bind("", self.key_release_events) - - self.bind("", self.select_all) - self.bind("", self.multi_selection) - - self.bind("", self.auto_completion.move_up) - self.bind("", self.auto_completion.move_down) - - self.bind("", - lambda e: self.handle_ctrl_hmovement()) - self.bind("", - lambda e: self.handle_ctrl_hmovement(True)) - - def key_release_events(self, event): - self.show_autocomplete(event) - self.update_current_line() - - match event.keysym: - # bracket pair completions - case "braceleft": - self.complete_bracket("}") - case "bracketleft": - self.complete_bracket("]") - case "parenleft": - self.complete_bracket(")") - - # surroundings for selection - case "apostrophe": - self.surrounding_selection("\'") - case "quotedbl": - self.surrounding_selection("\"") - - # autocompletion keys - case "Button-2" | "BackSpace" | "Escape" | "Control_L" | "Control_R" | "space": - self.hide_autocomplete() - case "rightarrow" | "leftarrow": - self.update_completions() - - # key events - case "Return": - self.enter_key_events() - case "Tab": - self.tab_key_events() - - # extra spaces - case ":" | ",": - self.insert(tk.INSERT, " ") - - case _: - pass - - def complete_bracket(self, bracket): - self.insert(tk.INSERT, bracket) - self.mark_set(tk.INSERT, "insert-1c") - - def surrounding_selection(self, char): - if self.tag_ranges(tk.SEL): - self.insert(char, tk.SEL_LAST) - self.insert(char, tk.SEL_FIRST) - - def enter_key_events(self): - if self.auto_completion.active: - self.auto_completion.choose() - return "break" - return self.check_indentation() - - def tab_key_events(self): - if self.auto_completion.active: - self.auto_completion.choose() - return "break" - - def move_to_next_word(self): - self.mark_set(tk.INSERT, self.index("insert+1c wordend")) - - def move_to_previous_word(self): - self.mark_set(tk.INSERT, self.index("insert-1c wordstart")) - - def handle_ctrl_hmovement(self, delta=False): - if delta: - self.move_to_next_word() - else: - self.move_to_previous_word() - - return "break" - - def get_all_text(self): - return self.get_all_text() - - def get_all_words(self): - return self.get_all_words() - - def get_current_word(self): - return self.current_word.strip() - - def cursor_screen_location(self): - pos_x, pos_y = self.winfo_rootx(), self.winfo_rooty() - - cursor = tk.INSERT - bbox = self.bbox(cursor) - if not bbox: - return (0, 0) - - bbx_x, bbx_y, _, bbx_h = bbox - return (pos_x + bbx_x - 1, pos_y + bbx_y + bbx_h) - - def show_autocomplete(self, event): - if not self.check_autocomplete_keys(event): - return - - if self.current_word.strip() not in ["{", "}", ":", "", None, "\""]: - if not self.auto_completion.active: - if event.keysym in ["Left", "Right"]: - return - pos = self.cursor_screen_location() - self.auto_completion.show(pos) - self.auto_completion.update_completions() - else: - self.auto_completion.update_completions() - else: - if self.auto_completion.active: - self.hide_autocomplete() - - def check_autocomplete_keys(self, event): - return True if event.keysym not in [ - "BackSpace", "Escape", "Return", "Tab", "space", - "Up", "Down", "Control_L", "Control_R"] else False - - def update_completion_words(self): - self.auto_completion.update_all_words() - - def update_completions(self): - self.auto_completion.update_completions() - - def hide_autocomplete(self): - self.auto_completion.hide() - - def move_cursor(self, position): - self.mark_set(tk.INSERT, position) - - def clear_all_selection(self): - self.tag_remove(tk.SEL, 1.0, tk.END) - - def select_line(self, line): - self.clear_all_selection() - - line = int(line.split(".")[0]) - start = str(float(line)) - end = str(float(line + 1)) - self.tag_add(tk.SEL, start, end) - - self.move_cursor(end) - - def update_current_indent(self): - line = self.get("insert linestart", "insert lineend") - match = re.match(r'^(\s+)', line) - self.current_indent = len(match.group(0)) if match else 0 - - def update_current_line(self): - self.current_line = self.get("insert linestart", "insert lineend") - return self.current_line - - def add_newline(self, count=1): - self.insert(tk.INSERT, "\n" * count) - - def confirm_autocomplete(self, text): - self.replace_current_word(text) - - def check_indentation(self, *args): - self.update_current_indent() - if self.update_current_line(): - if self.current_line[-1] in ["{", "[", ":", "("]: - self.current_indent += 4 - elif self.current_line[-1] in ["}", "]", ")"]: - self.current_indent -= 4 - - self.add_newline() - self.insert(tk.INSERT, " " * self.current_indent) - - self.update_current_indent() - - return "break" - - def get_origin(self): - return self.index("@0,0") - - def get_line_info(self, line): - return self.dlineinfo(line) - - def clear_insert(self, content): - self.delete(1.0, tk.END) - self.insert(1.0, content) - - def load_file(self, path): - with open(path, 'r') as fp: - self.clear_insert(fp.read()) - - self.mark_set(tk.INSERT, 1.0) - - def select_all(self, *args): - self.tag_remove("highlight", 1.0, tk.END) - - self.tag_add(tk.SEL, 1.0, tk.END) - - # scroll to top - # self.mark_set(tk.INSERT, 1.0) - # self.see(tk.INSERT) - - return "break" - - # def handle_space(self, *args): - # self.insert(tk.INSERT, "-") - - # return "break" - - def multi_selection(self, *args): - #TODO: multi cursor editing - - return "break" - - def replace_current_word(self, new_word): - if self.current_word.startswith("\n"): - self.delete("insert-1c wordstart+1c", "insert") - else: - self.delete("insert-1c wordstart", "insert") - self.insert("insert", new_word) - - def get_all_text(self, *args): - return self.get(1.0, tk.END) - - def get_all_text_ac(self, *args): - return self.get(1.0, "insert-1c wordstart-1c") + self.get("insert+1c", tk.END) - - def get_all_words(self, *args): - return self.words - - def update_words(self): - self.words = re.findall(r"\w+", self.get_all_text_ac()) - self.update_completion_words() - - def highlight_current_word(self): - self.tag_remove("highlight", 1.0, tk.END) - text = self.get("insert wordstart", "insert wordend") - word = re.findall(r"\w+", text) - if any(word): - if word[0] not in self.keywords: - self.highlighter.highlight_pattern(f"\\y{word[0]}\\y", "highlight", regexp=True) - - def on_change(self, *args): - self.current_word = self.get("insert-1c wordstart", "insert") - self.update_words() - self.highlight_current_word() - - - def config_tags(self): - self.tag_config(tk.SEL, background="#264f78", foreground="#d4d4d4") - self.tag_config("highlight", background="#464646", foreground="#d4d4d4") diff --git a/cupcake/editor/textw.py b/cupcake/editor/textw.py deleted file mode 100644 index a17d493..0000000 --- a/cupcake/editor/textw.py +++ /dev/null @@ -1,86 +0,0 @@ -import re, tkinter as tk - - -class TextW(tk.Text): - def __init__(self, master, *args, **kwargs): - super().__init__(master, *args, **kwargs) - self.master = master - - self.keywords = master.syntax.keywords - self.current_word = None - self.words = [] - - self._orig = self._w + "_orig" - self.tk.call("rename", self._w, self._orig) - self.tk.createcommand(self._w, self._proxy) - - self.config_appearance() - self.config_tags() - self.config_bindings() - - def multi_selection(self, *args): - #TODO: multi cursor editing - - return "break" - - def replace_current_word(self, new_word): - if self.current_word.startswith("\n"): - self.delete("insert-1c wordstart+1c", "insert") - else: - self.delete("insert-1c wordstart", "insert") - self.insert("insert", new_word) - - def get_all_text(self, *args): - return self.get(1.0, tk.END) - - def get_all_text_ac(self, *args): - return self.get(1.0, "insert-1c wordstart-1c") + self.get("insert+1c", tk.END) - - def get_all_words(self, *args): - return self.words - - def update_words(self): - self.words = re.findall(r"\w+", self.get_all_text_ac()) - self.master.update_completion_words() - - def highlight_current_word(self): - self.tag_remove("highlight", 1.0, tk.END) - text = self.get("insert wordstart", "insert wordend") - word = re.findall(r"\w+", text) - if any(word): - if word[0] not in self.keywords: - self.master.highlighter.highlight_pattern(f"\\y{word[0]}\\y", "highlight", regexp=True) - - def on_change(self, *args): - self.current_word = self.get("insert-1c wordstart", "insert") - self.update_words() - self.highlight_current_word() - - def config_appearance(self): - self.config( - font=self.master.font, bg="#1e1e1e", - fg="#d4d4d4", wrap=tk.NONE, relief=tk.FLAT, - highlightthickness=0, insertbackground="#aeafad") - - def config_tags(self): - self.tag_config(tk.SEL, background="#264f78", foreground="#d4d4d4") - self.tag_config("highlight", background="#464646", foreground="#d4d4d4") - - def config_bindings(self): - self.bind("", self.master.select_all) - self.bind("", self.multi_selection) - - def _proxy(self, *args): - cmd = (self._orig,) + args - result = self.tk.call(cmd) - - if (args[0] in ("insert", "replace", "delete") or - args[0:3] == ("mark", "set", "insert") or - args[0:2] == ("xview", "moveto") or - args[0:2] == ("xview", "scroll") or - args[0:2] == ("yview", "moveto") or - args[0:2] == ("yview", "scroll") - ): - self.event_generate("<>", when="tail") - - return result \ No newline at end of file diff --git a/cupcake/imageviewer/__init__.py b/cupcake/imageviewer/__init__.py new file mode 100644 index 0000000..1982178 --- /dev/null +++ b/cupcake/imageviewer/__init__.py @@ -0,0 +1,19 @@ +import tkinter as tk + +from PIL import Image, ImageTk +from ..editor import BaseEditor + + +#TODO: zooming in and out +class ImageViewer(BaseEditor): + def __init__(self, master, path, *args, **kwargs): + super().__init__(master, path, editable=False, *args, **kwargs) + self.open_image() + + def open_image(self): + self.image = Image.open(self.path) + self.image.thumbnail((700, 700)) + self.tk_image = ImageTk.PhotoImage(self.image) + + self.image_label = tk.Label(self, image=self.tk_image, bg=self.base.theme.background) + self.image_label.pack(fill=tk.BOTH, expand=True) diff --git a/cupcake/languages.py b/cupcake/languages.py new file mode 100644 index 0000000..4bb88a7 --- /dev/null +++ b/cupcake/languages.py @@ -0,0 +1,562 @@ +class Languages: + """ + Holds all available languages. To be passed to Editor during initialization. + """ + ABAP = "abap" + AMDGPU = "amdgpu" + APL = "apl" + ABNF = "abnf" + ACTIONSCRIPT3 = "actionscript3" + ACTIONSCRIPT = "actionscript" + ADA = "ada" + ADL = "adl" + AGDA = "agda" + AHEUI = "aheui" + ALLOY = "alloy" + AMBIENTTALK = "ambienttalk" + AMPL = "ampl" + HTML_NG2 = "html+ng2" + NG2 = "ng2" + ANTLR_ACTIONSCRIPT = "antlr-actionscript" + ANTLR_CSHARP = "antlr-csharp" + ANTLR_CPP = "antlr-cpp" + ANTLR_JAVA = "antlr-java" + ANTLR = "antlr" + ANTLR_OBJC = "antlr-objc" + ANTLR_PERL = "antlr-perl" + ANTLR_PYTHON = "antlr-python" + ANTLR_RUBY = "antlr-ruby" + APACHECONF = "apacheconf" + APPLESCRIPT = "applescript" + ARDUINO = "arduino" + ARROW = "arrow" + ARTURO = "arturo" + ASC = "asc" + ASPECTJ = "aspectj" + ASYMPTOTE = "asymptote" + AUGEAS = "augeas" + AUTOIT = "autoit" + AUTOHOTKEY = "autohotkey" + AWK = "awk" + BBCBASIC = "bbcbasic" + BBCODE = "bbcode" + BC = "bc" + BST = "bst" + BARE = "bare" + BASEMAKE = "basemake" + BASH = "bash" + CONSOLE = "console" + BATCH = "batch" + BDD = "bdd" + BEFUNGE = "befunge" + BERRY = "berry" + BIBTEX = "bibtex" + BLITZBASIC = "blitzbasic" + BLITZMAX = "blitzmax" + BNF = "bnf" + BOA = "boa" + BOO = "boo" + BOOGIE = "boogie" + BRAINFUCK = "brainfuck" + BUGS = "bugs" + CAMKES = "camkes" + C = "c" + CMAKE = "cmake" + C_OBJDUMP = "c-objdump" + CPSA = "cpsa" + CSS_UL4 = "css+ul4" + ASPX_CS = "aspx-cs" + CSHARP = "csharp" + CA65 = "ca65" + CADL = "cadl" + CAPDL = "capdl" + CAPNP = "capnp" + CBMBAS = "cbmbas" + CDDL = "cddl" + CEYLON = "ceylon" + CFENGINE3 = "cfengine3" + CHAISCRIPT = "chaiscript" + CHAPEL = "chapel" + CHARMCI = "charmci" + HTML_CHEETAH = "html+cheetah" + JAVASCRIPT_CHEETAH = "javascript+cheetah" + CHEETAH = "cheetah" + XML_CHEETAH = "xml+cheetah" + CIRRU = "cirru" + CLAY = "clay" + CLEAN = "clean" + CLOJURE = "clojure" + CLOJURESCRIPT = "clojurescript" + COBOLFREE = "cobolfree" + COBOL = "cobol" + COFFEESCRIPT = "coffeescript" + CFC = "cfc" + CFM = "cfm" + CFS = "cfs" + COMAL = "comal" + COMMON_LISP = "common-lisp" + COMPONENTPASCAL = "componentpascal" + COQ = "coq" + CPLINT = "cplint" + CPP = "cpp" + CPP_OBJDUMP = "cpp-objdump" + CRMSH = "crmsh" + CROC = "croc" + CRYPTOL = "cryptol" + CR = "cr" + CSOUND_DOCUMENT = "csound-document" + CSOUND = "csound" + CSOUND_SCORE = "csound-score" + CSS_DJANGO = "css+django" + CSS_RUBY = "css+ruby" + CSS_GENSHITEXT = "css+genshitext" + CSS = "css" + CSS_PHP = "css+php" + CSS_SMARTY = "css+smarty" + CUDA = "cuda" + CYPHER = "cypher" + CYTHON = "cython" + D = "d" + D_OBJDUMP = "d-objdump" + DPATCH = "dpatch" + DART = "dart" + DASM16 = "dasm16" + DEBCONTROL = "debcontrol" + DELPHI = "delphi" + DEVICETREE = "devicetree" + DG = "dg" + DIFF = "diff" + DJANGO = "django" + DOCKER = "docker" + DTD = "dtd" + DUEL = "duel" + DYLAN_CONSOLE = "dylan-console" + DYLAN = "dylan" + DYLAN_LID = "dylan-lid" + ECL = "ecl" + EC = "ec" + EARL_GREY = "earl-grey" + EASYTRIEVE = "easytrieve" + EBNF = "ebnf" + EIFFEL = "eiffel" + IEX = "iex" + ELIXIR = "elixir" + ELM = "elm" + ELPI = "elpi" + EMACS_LISP = "emacs-lisp" + EMAIL = "email" + ERB = "erb" + ERLANG = "erlang" + ERL = "erl" + HTML_EVOQUE = "html+evoque" + EVOQUE = "evoque" + XML_EVOQUE = "xml+evoque" + EXECLINE = "execline" + EZHIL = "ezhil" + FSHARP = "fsharp" + FSTAR = "fstar" + FACTOR = "factor" + FANCY = "fancy" + FAN = "fan" + FELIX = "felix" + FENNEL = "fennel" + FIFT = "fift" + FISH = "fish" + FLATLINE = "flatline" + FLOSCRIPT = "floscript" + FORTH = "forth" + FORTRANFIXED = "fortranfixed" + FORTRAN = "fortran" + FOXPRO = "foxpro" + FREEFEM = "freefem" + FUNC = "func" + FUTHARK = "futhark" + GAP_CONSOLE = "gap-console" + GAP = "gap" + GDSCRIPT = "gdscript" + GLSL = "glsl" + GSQL = "gsql" + GAS = "gas" + GCODE = "gcode" + GENSHI = "genshi" + GENSHITEXT = "genshitext" + POT = "pot" + GHERKIN = "gherkin" + GNUPLOT = "gnuplot" + GO = "go" + GOLO = "golo" + GOODDATA_CL = "gooddata-cl" + GOSU = "gosu" + GST = "gst" + GRAPHVIZ = "graphviz" + GROFF = "groff" + GROOVY = "groovy" + HLSL = "hlsl" + HTML_UL4 = "html+ul4" + HAML = "haml" + HTML_HANDLEBARS = "html+handlebars" + HANDLEBARS = "handlebars" + HASKELL = "haskell" + HAXE = "haxe" + HEXDUMP = "hexdump" + HSAIL = "hsail" + HSPEC = "hspec" + HTML_DJANGO = "html+django" + HTML_GENSHI = "html+genshi" + HTML = "html" + HTML_PHP = "html+php" + HTML_SMARTY = "html+smarty" + HTTP = "http" + HAXEML = "haxeml" + HYLANG = "hylang" + HYBRIS = "hybris" + IDL = "idl" + ICON = "icon" + IDRIS = "idris" + IGOR = "igor" + INFORM6 = "inform6" + I6T = "i6t" + INFORM7 = "inform7" + INI = "ini" + IO = "io" + IOKE = "ioke" + IRC = "irc" + ISABELLE = "isabelle" + J = "j" + JMESPATH = "jmespath" + JSLT = "jslt" + JAGS = "jags" + JASMIN = "jasmin" + JAVA = "java" + JAVASCRIPT_DJANGO = "javascript+django" + JAVASCRIPT_RUBY = "javascript+ruby" + JS_GENSHITEXT = "js+genshitext" + JAVASCRIPT = "javascript" + JAVASCRIPT_PHP = "javascript+php" + JAVASCRIPT_SMARTY = "javascript+smarty" + JS_UL4 = "js+ul4" + JCL = "jcl" + JSGF = "jsgf" + JSONLD = "jsonld" + JSON = "json" + JSONNET = "jsonnet" + JSP = "jsp" + JLCON = "jlcon" + JULIA = "julia" + JUTTLE = "juttle" + K = "k" + KAL = "kal" + KCONFIG = "kconfig" + KMSG = "kmsg" + KOKA = "koka" + KOTLIN = "kotlin" + KUIN = "kuin" + LSL = "lsl" + CSS_LASSO = "css+lasso" + HTML_LASSO = "html+lasso" + JAVASCRIPT_LASSO = "javascript+lasso" + LASSO = "lasso" + XML_LASSO = "xml+lasso" + LEAN = "lean" + LESS = "less" + LIGHTTPD = "lighttpd" + LILYPOND = "lilypond" + LIMBO = "limbo" + LIQUID = "liquid" + LITERATE_AGDA = "literate-agda" + LITERATE_CRYPTOL = "literate-cryptol" + LITERATE_HASKELL = "literate-haskell" + LITERATE_IDRIS = "literate-idris" + LIVESCRIPT = "livescript" + LLVM = "llvm" + LLVM_MIR_BODY = "llvm-mir-body" + LLVM_MIR = "llvm-mir" + LOGOS = "logos" + LOGTALK = "logtalk" + LUA = "lua" + MCFUNCTION = "mcfunction" + MCSCHEMA = "mcschema" + MIME = "mime" + MIPS = "mips" + MOOCODE = "moocode" + DOSCON = "doscon" + MACAULAY2 = "macaulay2" + MAKE = "make" + CSS_MAKO = "css+mako" + HTML_MAKO = "html+mako" + JAVASCRIPT_MAKO = "javascript+mako" + MAKO = "mako" + XML_MAKO = "xml+mako" + MAQL = "maql" + MARKDOWN = "markdown" + MASK = "mask" + MASON = "mason" + MATHEMATICA = "mathematica" + MATLAB = "matlab" + MATLABSESSION = "matlabsession" + MAXIMA = "maxima" + MESON = "meson" + MINID = "minid" + MINISCRIPT = "miniscript" + MODELICA = "modelica" + MODULA2 = "modula2" + TRAC_WIKI = "trac-wiki" + MONKEY = "monkey" + MONTE = "monte" + MOONSCRIPT = "moonscript" + MOSEL = "mosel" + CSS_MOZPREPROC = "css+mozpreproc" + MOZHASHPREPROC = "mozhashpreproc" + JAVASCRIPT_MOZPREPROC = "javascript+mozpreproc" + MOZPERCENTPREPROC = "mozpercentpreproc" + XUL_MOZPREPROC = "xul+mozpreproc" + MQL = "mql" + MSCGEN = "mscgen" + MUPAD = "mupad" + MXML = "mxml" + MYSQL = "mysql" + CSS_MYGHTY = "css+myghty" + HTML_MYGHTY = "html+myghty" + JAVASCRIPT_MYGHTY = "javascript+myghty" + MYGHTY = "myghty" + XML_MYGHTY = "xml+myghty" + NCL = "ncl" + NSIS = "nsis" + NASM = "nasm" + OBJDUMP_NASM = "objdump-nasm" + NEMERLE = "nemerle" + NESC = "nesc" + NESTEDTEXT = "nestedtext" + NEWLISP = "newlisp" + NEWSPEAK = "newspeak" + NGINX = "nginx" + NIMROD = "nimrod" + NIT = "nit" + NIXOS = "nixos" + NODEJSREPL = "nodejsrepl" + NOTMUCH = "notmuch" + NUSMV = "nusmv" + NUMPY = "numpy" + OBJDUMP = "objdump" + OBJECTIVE_C = "objective-c" + OBJECTIVE_C__ = "objective-c++" + OBJECTIVE_J = "objective-j" + OCAML = "ocaml" + OCTAVE = "octave" + ODIN = "odin" + OMG_IDL = "omg-idl" + OOC = "ooc" + OPA = "opa" + OPENEDGE = "openedge" + OUTPUT = "output" + PACMANCONF = "pacmanconf" + PAN = "pan" + PARASAIL = "parasail" + PAWN = "pawn" + PEG = "peg" + PERL6 = "perl6" + PERL = "perl" + PHIX = "phix" + PHP = "php" + PIG = "pig" + PIKE = "pike" + PKGCONFIG = "pkgconfig" + PLPGSQL = "plpgsql" + POINTLESS = "pointless" + PONY = "pony" + PORTUGOL = "portugol" + POSTSCRIPT = "postscript" + PSQL = "psql" + POSTGRESQL = "postgresql" + POV = "pov" + POWERSHELL = "powershell" + PWSH_SESSION = "pwsh-session" + PRAAT = "praat" + PROCFILE = "procfile" + PROLOG = "prolog" + PROMQL = "promql" + PROPERTIES = "properties" + PROTOBUF = "protobuf" + PSYSH = "psysh" + PUG = "pug" + PUPPET = "puppet" + PYPYLOG = "pypylog" + PYTHON2 = "python2" + PY2TB = "py2tb" + PYCON = "pycon" + PYTHON = "python" + PYTB = "pytb" + PY_UL4 = "py+ul4" + QBASIC = "qbasic" + Q = "q" + QVTO = "qvto" + QLIK = "qlik" + QML = "qml" + RCONSOLE = "rconsole" + RNG_COMPACT = "rng-compact" + SPEC = "spec" + RACKET = "racket" + RAGEL_C = "ragel-c" + RAGEL_CPP = "ragel-cpp" + RAGEL_D = "ragel-d" + RAGEL_EM = "ragel-em" + RAGEL_JAVA = "ragel-java" + RAGEL = "ragel" + RAGEL_OBJC = "ragel-objc" + RAGEL_RUBY = "ragel-ruby" + RD = "rd" + REASONML = "reasonml" + REBOL = "rebol" + RED = "red" + REDCODE = "redcode" + REGISTRY = "registry" + RESOURCEBUNDLE = "resourcebundle" + REXX = "rexx" + RHTML = "rhtml" + RIDE = "ride" + RITA = "rita" + ROBOCONF_GRAPH = "roboconf-graph" + ROBOCONF_INSTANCES = "roboconf-instances" + ROBOTFRAMEWORK = "robotframework" + RQL = "rql" + RSL = "rsl" + RESTRUCTUREDTEXT = "restructuredtext" + TRAFFICSCRIPT = "trafficscript" + RBCON = "rbcon" + RUBY = "ruby" + RUST = "rust" + SAS = "sas" + SPLUS = "splus" + SML = "sml" + SNBT = "snbt" + SARL = "sarl" + SASS = "sass" + SAVI = "savi" + SCALA = "scala" + SCAML = "scaml" + SCDOC = "scdoc" + SCHEME = "scheme" + SCILAB = "scilab" + SCSS = "scss" + SED = "sed" + SHEXC = "shexc" + SHEN = "shen" + SIEVE = "sieve" + SILVER = "silver" + SINGULARITY = "singularity" + SLASH = "slash" + SLIM = "slim" + SLURM = "slurm" + SMALI = "smali" + SMALLTALK = "smalltalk" + SGF = "sgf" + SMARTY = "smarty" + SMITHY = "smithy" + SNOBOL = "snobol" + SNOWBALL = "snowball" + SOLIDITY = "solidity" + SOPHIA = "sophia" + SP = "sp" + DEBSOURCES = "debsources" + SPARQL = "sparql" + SPICE = "spice" + SQL_JINJA = "sql+jinja" + SQL = "sql" + SQLITE3 = "sqlite3" + SQUIDCONF = "squidconf" + SRCINFO = "srcinfo" + SSP = "ssp" + STAN = "stan" + STATA = "stata" + SUPERCOLLIDER = "supercollider" + SWIFT = "swift" + SWIG = "swig" + SYSTEMVERILOG = "systemverilog" + TAP = "tap" + TNT = "tnt" + TOML = "toml" + TADS3 = "tads3" + TAL = "tal" + TASM = "tasm" + TCL = "tcl" + TCSH = "tcsh" + TCSHCON = "tcshcon" + TEA = "tea" + TEAL = "teal" + TERATERMMACRO = "teratermmacro" + TERMCAP = "termcap" + TERMINFO = "terminfo" + TERRAFORM = "terraform" + TEX = "tex" + TEXT = "text" + TI = "ti" + THRIFT = "thrift" + TID = "tid" + TLB = "tlb" + TODOTXT = "todotxt" + TSQL = "tsql" + TREETOP = "treetop" + TURTLE = "turtle" + HTML_TWIG = "html+twig" + TWIG = "twig" + TYPESCRIPT = "typescript" + TYPOSCRIPTCSSDATA = "typoscriptcssdata" + TYPOSCRIPTHTMLDATA = "typoscripthtmldata" + TYPOSCRIPT = "typoscript" + UL4 = "ul4" + UCODE = "ucode" + UNICON = "unicon" + UNIXCONFIG = "unixconfig" + URBISCRIPT = "urbiscript" + USD = "usd" + VBSCRIPT = "vbscript" + VCL = "vcl" + VCLSNIPPETS = "vclsnippets" + VCTREESTATUS = "vctreestatus" + VGL = "vgl" + VALA = "vala" + ASPX_VB = "aspx-vb" + VB_NET = "vb.net" + HTML_VELOCITY = "html+velocity" + VELOCITY = "velocity" + XML_VELOCITY = "xml+velocity" + VERILOG = "verilog" + VHDL = "vhdl" + VIM = "vim" + WDIFF = "wdiff" + WAST = "wast" + WEBIDL = "webidl" + WHILEY = "whiley" + WOWTOC = "wowtoc" + WREN = "wren" + X10 = "x10" + XML_UL4 = "xml+ul4" + XQUERY = "xquery" + XML_DJANGO = "xml+django" + XML_RUBY = "xml+ruby" + XML = "xml" + XML_PHP = "xml+php" + XML_SMARTY = "xml+smarty" + XORG_CONF = "xorg.conf" + XSLT = "xslt" + XTEND = "xtend" + EXTEMPORE = "extempore" + YAML_JINJA = "yaml+jinja" + YAML = "yaml" + YANG = "yang" + ZEEK = "zeek" + ZEPHIR = "zephir" + ZIG = "zig" + ANSYS = "ansys" + IPYTHON2 = "ipython2" + IPYTHON3 = "ipython3" + IPYTHONCONSOLE = "ipythonconsole" + + +# from pygments.lexers import get_all_lexers, get_lexer_by_name +# for lexer in get_all_lexers(): +# try: +# name = lexer[1][0] +# print(f"{name.upper().replace('+', '_').replace('-', '_')} = \"{name}\"", ) +# except: +# pass diff --git a/cupcake/texteditor/__init__.py b/cupcake/texteditor/__init__.py new file mode 100644 index 0000000..f1b3ed4 --- /dev/null +++ b/cupcake/texteditor/__init__.py @@ -0,0 +1,240 @@ +import tkinter as tk + +from ..utils import Scrollbar +from ..editor import BaseEditor + +from .minimap import Minimap +from .linenumbers import LineNumbers +from .text import Text + + +class TextEditor(BaseEditor): + def __init__(self, master, path=None, language=None, font=None, minimalist=False, *args, **kwargs): + super().__init__(master, path, *args, **kwargs) + self.font = font or self.base.settings.font + self.minimalist = minimalist + self.language = language + + self.rowconfigure(0, weight=1) + self.columnconfigure(1, weight=1) + + self.text = Text(self, path=self.path, minimalist=minimalist, language=language) + self.linenumbers = LineNumbers(self, self.text, self.font) + self.scrollbar = Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview, style="EditorScrollbar") + + self.text.config(font=self.font) + self.text.configure(yscrollcommand=self.scrollbar.set) + + if not self.minimalist: + self.minimap = Minimap(self, self.text) + self.minimap.grid(row=0, column=2, sticky=tk.NS) + + self.linenumbers.grid(row=0, column=0, sticky=tk.NS) + self.text.grid(row=0, column=1, sticky=tk.NSEW) + self.scrollbar.grid(row=0, column=3, sticky=tk.NS) + + self.text.bind("<>", self.on_change) + self.text.bind("<>", self.on_scroll) + + def on_change(self, *_): + self.text.refresh() + self.linenumbers.redraw() + if not self.minimalist: + self.minimap.redraw_cursor() + + def on_scroll(self, *_): + self.linenumbers.redraw() + if not self.minimalist: + self.minimap.redraw() + + def unsupported_file(self): + self.text.highlighter.lexer = None + self.text.show_unsupported_dialog() + self.linenumbers.grid_remove() + self.scrollbar.grid_remove() + self.editable = False + + def focus(self): + self.text.focus() + self.on_change() + + def set_fontsize(self, size): + self.font.configure(size=size) + self.linenumbers.set_bar_width(size * 3) + self.on_change() + + def save(self, path=None): + if self.editable: + self.text.save_file(path) + + def cut(self, *_): + if self.editable: + self.text.cut() + + def copy(self, *_): + if self.editable: + self.text.copy() + + def paste(self, *_): + if self.editable: + self.text.paste() + + def write(self, *args, **kwargs): + if self.editable: + self.text.write(*args, **kwargs) + + def insert(self, *args, **kwargs): + if self.editable: + self.text.insert(*args, **kwargs) + + def get(self, *args, **kwargs): + if self.editable: + self.text.get(*args, **kwargs) + + def clear(self): + self.delete("1.0", tk.END) + + def delete(self, *args, **kwargs): + if self.editable: + self.text.delete(*args, **kwargs) + + def mark_set(self, *args, **kwargs): + if self.editable: + self.text.mark_set(*args, **kwargs) + + def compare(self, *args, **kwargs): + return self.text.compare(*args, **kwargs) + + def dlineinfo(self, index): + return self.text.dlineinfo(index) + + def edit_modified(self, arg=None): + return self.text.edit_modified(arg) + + def edit_redo(self): + if self.editable: + self.text.edit_redo() + + def edit_reset(self): + if self.editable: + self.text.edit_reset() + + def edit_separator(self): + if self.editable: + self.text.edit_separator() + + def edit_undo(self): + if self.editable: + self.text.edit_undo() + + def image_create(self, index, **kwargs): + if self.editable: + return self.text.image_create(index, **kwargs) + + def image_cget(self, index, option): + return self.text.image_cget(index, option) + + def image_configure(self, index, **kwargs): + if self.editable: + return self.text.image_configure(index, **kwargs) + + def image_names(self): + return self.text.image_names() + + def index(self, i): + return self.text.index(i) + + def mark_gravity(self, mark, gravity=None): + return self.text.mark_gravity(mark, gravity) + + def mark_names(self): + return self.text.mark_names() + + def mark_next(self, index): + return self.text.mark_next(index) + + def mark_previous(self, index): + return self.text.mark_previous(index) + + def mark_set(self, mark, index): + if self.editable: + self.text.mark_set(mark, index) + + def mark_unset(self, mark): + if self.editable: + self.text.mark_unset(mark) + + def scan_dragto(self, x, y): + self.text.scan_dragto(x, y) + + def scan_mark(self, x, y): + self.text.scan_mark(x, y) + + def search(self, pattern, index, **kwargs): + return self.text.search(pattern, index, **kwargs) + + def see(self, index): + self.text.see(index) + + def tag_add(self, tagName, index1, index2=None): + if self.editable: + self.text.tag_add(tagName, index1, index2) + + def tag_bind(self, tagName, sequence, func, add=None): + self.text.tag_bind(tagName, sequence, func, add) + + def tag_cget(self, tagName, option): + return self.text.tag_cget(tagName, option) + + def tag_config(self, tagName, **kwargs): + if self.editable: + self.text.tag_config(tagName, **kwargs) + + def tag_names(self, index=None): + return self.text.tag_names(index) + + def tag_nextrange(self, tagName, index1, index2=None): + return self.text.tag_nextrange(tagName, index1, index2) + + def tag_prevrange(self, tagName, index1, index2=None): + return self.text.tag_prevrange(tagName, index1, index2) + + def tag_raise(self, tagName, aboveThis=None): + if self.editable: + self.text.tag_raise(tagName, aboveThis) + + def tag_ranges(self, tagName): + return self.text.tag_ranges(tagName) + + def tag_remove(self, tagName, index1, index2=None): + if self.editable: + self.text.tag_remove(tagName, index1, index2) + + def tag_unbind(self, tagName, sequence, funcid=None): + self.text.tag_unbind(tagName, sequence, funcid) + + def window_cget(self, index, option): + return self.text.window_cget(index, option) + + def window_configure(self, index, **kwargs): + if self.editable: + self.text.window_configure(index, **kwargs) + + def window_create(self, index, **kwargs): + if self.editable: + self.text.window_create(index, **kwargs) + + def window_names(self): + return self.text.window_names() + + def xview_moveto(self, fraction): + self.text.xview_moveto(fraction) + + def xview_scroll(self, n, what): + self.text.xview_scroll(n, what) + + def yview_moveto(self, fraction): + self.text.yview_moveto(fraction) + + def yview_scroll(self, n, what): + self.text.yview_scroll(n, what) diff --git a/cupcake/editor/autocomplete/__init__.py b/cupcake/texteditor/autocomplete/__init__.py similarity index 71% rename from cupcake/editor/autocomplete/__init__.py rename to cupcake/texteditor/autocomplete/__init__.py index 7eeba89..bd605e2 100644 --- a/cupcake/editor/autocomplete/__init__.py +++ b/cupcake/texteditor/autocomplete/__init__.py @@ -1,42 +1,35 @@ import tkinter as tk +from itertools import chain -from .itemkinds import Kinds +from .kinds import Kinds from .item import AutoCompleteItem +from ...utils import Toplevel -class AutoComplete(tk.Toplevel): + +class AutoComplete(Toplevel): def __init__(self, master, items=None, active=False, *args, **kwargs): super().__init__(master, *args, **kwargs) - self.master = master - self.autocomplete_kinds = Kinds(self) - self.config(bg="#454545", padx=1, pady=1) + self.config(padx=1, pady=1, bg=self.base.theme.border) self.active = active - self.font = self.master.font - if not self.active: self.withdraw() self.overrideredirect(True) self.wm_attributes("-topmost", True) - self.grid_columnconfigure(0, weight=1) self.menu_items = [] self.active_items = [] - self.row = 0 self.selected = 0 - if items: - self.items = items - # [(completion, type), ...] - + # TODO this should be a dict + self.items = items # [(completion, type), ...] self.add_all_items() self.refresh_selected() - - self.configure_bindings() def update_completions(self): self.refresh_geometry() @@ -45,24 +38,28 @@ def update_completions(self): term = self.master.get_current_word() - new = [i for i in self.get_items() if i.get_text() == term] - new += [i for i in self.get_items() if i.get_text().startswith(term)] - new += [i for i in self.get_items() if term in i.get_text() and i.get_kind() != "word" and i not in new] - new += [i for i in self.get_items() if term in i.get_text() and i not in new] + exact, starts, includes = [], [], [] + for i in self.menu_items: + if i.get_text() == term: + exact.append(i) + elif i.get_text().startswith(term): + starts.append(i) + elif term in i.get_text(): + includes.append(i) + new = list(chain(exact, starts, includes)) self.hide_all_items() - if any(new): self.show_items(new[:10] if len(new) > 10 else new, term) else: self.hide() - def move_up(self, *args): + def move_up(self, *_): if self.active: self.select(-1) return "break" - def move_down(self, *args): + def move_down(self, *_): if self.active: self.select(1) return "break" @@ -75,22 +72,16 @@ def add_all_items(self): self.refresh_selected() def update_all_words(self): - for word in self.master.get_all_words(): + for word in self.master.words: if word not in self.get_items_text(): self.add_item(word, "word") - for word in self.get_items(): - if word.get_text() not in self.master.get_all_words() and word.get_kind() == "word": + for word in self.menu_items: + if word.get_text() not in self.master.words and word.get_kind() == "word": self.remove_item(word) - def configure_bindings(self): - # root.bind("" , self.hide) - # root.bind("", self.refresh_geometry) - # root.bind("", self.hide) - ... - - def add_item(self, left, kind=None): - new_item = AutoCompleteItem(self, left, kind=kind) + def add_item(self, text, kind=None): + new_item = AutoCompleteItem(self, text, kind=kind) new_item.grid(row=self.row, sticky=tk.EW) self.menu_items.append(new_item) @@ -120,9 +111,6 @@ def refresh_selected(self): i.deselect() if self.selected < len(self.active_items): self.active_items[self.selected].select() - - def get_items(self): - return self.menu_items def get_items_text(self): return [i.get_text() for i in self.menu_items] @@ -144,7 +132,7 @@ def show_items(self, items, term): self.reset_selection() - def refresh_geometry(self, *args): + def refresh_geometry(self, *_): self.update_idletasks() self.geometry("+{}+{}".format(*self.master.cursor_screen_location())) @@ -154,7 +142,7 @@ def show(self, pos): self.geometry("+{}+{}".format(*pos)) self.deiconify() - def hide(self, *args): + def hide(self, *_): self.active = False self.withdraw() self.reset() @@ -162,10 +150,13 @@ def hide(self, *args): def reset(self): self.reset_selection() - def choose(self, this=None, *args): - self.hide() + def choose(self, this=None, *_): + if not self.active_items: + return + if not this: this = self.active_items[self.selected] self.master.confirm_autocomplete(this.get_text()) + self.hide() return "break" diff --git a/cupcake/texteditor/autocomplete/item.py b/cupcake/texteditor/autocomplete/item.py new file mode 100644 index 0000000..e969073 --- /dev/null +++ b/cupcake/texteditor/autocomplete/item.py @@ -0,0 +1,81 @@ +import tkinter as tk + +from .kind import Kind +from ...utils import Frame + + +class AutoCompleteItem(Frame): + def __init__(self, master, text, kind=None, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.config(width=400, bg=self.base.theme.autocomplete["background"]) + self.bg, self.fg, self.hbg, self.hfg = self.base.theme.autocomplete.values() + + self.text = text + self.kind = kind + + self.kindw = Kind(self, self.master.autocomplete_kinds, kind) + self.textw = tk.Text(self, + font=("Consolas", 11), fg=self.fg, bg=self.bg, + relief=tk.FLAT, highlightthickness=0, width=30, height=1) + self.textw.insert(tk.END, text) + self.textw.config(state=tk.DISABLED) + + self.textw.tag_config("term", foreground=self.base.theme.accent) + + self.kindw.bind("", self.on_click) + self.textw.bind("", self.on_click) + + self.bind("", self.on_hover) + self.bind("", self.off_hover) + + self.selected = False + self.hovered = False + + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(0, weight=1) + + self.kindw.grid(row=0, column=0, sticky=tk.NSEW) + self.textw.grid(row=0, column=1, sticky=tk.NSEW) + + def get_text(self): + return self.text + + def get_kind(self): + return self.kind + + def mark_term(self, term): + start_pos = self.text.find(term) + end_pos = start_pos + len(term) + self.textw.tag_remove("term", 1.0, tk.END) + self.textw.tag_add("term", f"1.{start_pos}", f"1.{end_pos}") + + def on_click(self, *args): + self.master.choose(self) + + def on_hover(self, *args): + if not self.selected: + self.kindw.config(bg=self.hbg) + self.textw.config(bg=self.hbg) + self.hovered = True + + def off_hover(self, *args): + if not self.selected: + self.kindw.config(bg=self.bg) + self.textw.config(bg=self.bg) + self.hovered = False + + def toggle_selection(self): + if self.selected: + self.select() + else: + self.deselect() + + def select(self): + self.kindw.config(bg=self.hbg) + self.textw.config(bg=self.hbg, fg=self.hfg) + self.selected = True + + def deselect(self): + self.kindw.config(bg=self.bg) + self.textw.config(bg=self.bg, fg=self.fg) + self.selected = False \ No newline at end of file diff --git a/cupcake/texteditor/autocomplete/kind.py b/cupcake/texteditor/autocomplete/kind.py new file mode 100644 index 0000000..46906ac --- /dev/null +++ b/cupcake/texteditor/autocomplete/kind.py @@ -0,0 +1,39 @@ +import tkinter as tk +from ...utils import Label + + +class Kind(Label): + def __init__(self, master, kinds, kind="text", *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.master = master + self.base = master.base + + self.kinds = kinds + self.kind = kind + + self.image = None + + self.config(bg=self.base.theme.autocomplete["background"]) + self.config_image() + + def config_image(self): + match self.kind: + case "method": + self.image = self.kinds.imethods + case "variable": + self.image = self.kinds.ivariables + case "field": + self.image = self.kinds.ifields + case "class": + self.image = self.kinds.iclasses + case "interface": + self.image = self.kinds.iinterfaces + case "module": + self.image = self.kinds.imodules + case "property": + self.image = self.kinds.iproperties + case "keyword": + self.image = self.kinds.ikeywords + case _: + self.image = self.kinds.iwords + self.config(image=self.image) \ No newline at end of file diff --git a/cupcake/editor/autocomplete/itemkinds.py b/cupcake/texteditor/autocomplete/kinds.py similarity index 70% rename from cupcake/editor/autocomplete/itemkinds.py rename to cupcake/texteditor/autocomplete/kinds.py index 2504849..f4a0b43 100644 --- a/cupcake/editor/autocomplete/itemkinds.py +++ b/cupcake/texteditor/autocomplete/kinds.py @@ -52,26 +52,26 @@ def __init__(self, master, *args, **kwargs): 2WtJ9TiOf6yZfDiEMDaz7SiKPhiApM9A2d0bGyY3gPJ8Pv8CS1/l7mEDXsgNYDwefwP2KpXKmXW4yPcajcbXvzh Jkn1JO+7+JMuyq6tgr9e75u5PJe00m82fC7Mnk8lt4FUI4fEqLOkR8LLoAfALta2R2TiY6xMAAAAASUVORK5CYI I=""") - self.iproperties = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAACXB - IWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAdFJREFUKJGN0r - 9rFEEUB/Dvd+8uxzUhoBCCEkhtEbTbHVdZIiQQfySKWmkjWtkIWtj4D1imsDQIRlCDoBbauOLNzR5aGrBVEY0g2 - gQCtzfztTALinenrxt4n++8Nwwxorrd7ni/379O8gyAKQDvAdwyxqwAQDQCTnrvLcndJBcBnAewC0Be9QzF3vub - JJ8aYy6GEGYkrZBcMMZsWGv3WWsvcxDM83xibGzsQ6/X29NoNPaTfExyPkmS1+12ezaKoucAfgy8udVqTZP8lGX - ZVlmW7yR9BLBYFMUBks9IXgEwM2zsr5Km8jyvZ1n2rSzLuRDCUgjhhaRLkt4C2ByIvfezANBsNicBoAqQtJym6R - NJ1wCs/7VzURRHQgh3oyg6Gcdxx1o7XZbl5yzL+t1ud9x7fwPA0RBC/Ad2zs1JWvsNZiQfAQgANgHsBfCQ5NUkS - b7XK2itzSStSToVx3GnCgJwnOSG936iVqt9SZJkuzLcgQdJrocQzqZp+tI5d1jS/eo85FHBTqdzCMADAKeNMa+c - c/OS7lSjD4MAUAdwG8C5HbggaRXAUhzHxSgI/PqeLUnbzrllSashhBPGmH9CAKhLukDynqQtAMfSNH3zPxAAfgL - 2/u9cQzl88QAAAABJRU5ErkJggg==""") - self.ikeywords = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAACXBIW - XMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAPRJREFUKJGlkrFK - xEAURc8bHltbWqytYG0TYiC2VnZ+hK3iByhouX7HfkcmhAgigpXFgt02gpXETObZiEVMWI23PecOl+HBPyLe+5c - xaGaLLMsWY1yBHRE5NLO3Hrtwzm0BFEVxJCI3Q2XM7BIIPbYLPAOo6n2M8exHWUROxmaFEJ4AkiRZA+sxb1KkLM - vrTVKM8SHLsmVVVfMY4ynwnqbplZrZbFPZOacATdOIqs7MrP8/E2Z774+HQAjhMc/zFUBVVfOu6/b7jgLnQ2VVv - QVWXw/ticigNzlS1/X2b+UQwkeapq/f69q2Hb3tgdwBB39ZN5pP4uJac+7GJRAAAAAASUVORK5CYII=""") - self.iwords = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAAA8AAAAICAYAAAAm06XyAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAA - GXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAATBJREFUGJWFjyFLQ2EYhc95t31JEQYiGExaLBO - DYbvJaFBEGBqHGNW4n6BFBUGw2ZUJCoaxMGT3Xm+1LwkLgrILwzLZdwy7wTD0iec9z4EXmEAcx40wDPcn3X5j/x - X+gmEYLgA4JLkE4KFSqdzEcdwA0Jc0A0CSzoMgSNrt9pRzrg5gRVLXSNZIfki6AnDa6XRKGBsbku5IPpNsJkky5 - 5x7AjAv6ZLkV344HJ4VCoUdM9uUNMrlcsuZfBEEwS0ARFG0670/BrDY6/XWq9XqCEAr75xrABgAeASwKilHElmG - bIgkpwF8ZuL45yiKUgCBpG+SLwCOSG5LmvXebwEomdk9yTVJife+ViwWm/1+f9tI1iW1ssIrgIGkd0lmZm9mdk1 - yr1wudwFUzewkTdOU5MEPhImOHsSTJnYAAAAASUVORK5CYII=""") + self.iproperties = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAA + AACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAdFJREFUK + JGN0r9rFEEUB/Dvd+8uxzUhoBCCEkhtEbTbHVdZIiQQfySKWmkjWtkIWtj4D1imsDQIRlCDoBbauOLNzR5aGrBV + EY0g2gQCtzfztTALinenrxt4n++8Nwwxorrd7ni/379O8gyAKQDvAdwyxqwAQDQCTnrvLcndJBcBnAewC0Be9Qz + F3vubJJ8aYy6GEGYkrZBcMMZsWGv3WWsvcxDM83xibGzsQ6/X29NoNPaTfExyPkmS1+12ezaKoucAfgy8udVqTZ + P8lGXZVlmW7yR9BLBYFMUBks9IXgEwM2zsr5Km8jyvZ1n2rSzLuRDCUgjhhaRLkt4C2ByIvfezANBsNicBoAqQt + Jym6RNJ1wCs/7VzURRHQgh3oyg6Gcdxx1o7XZbl5yzL+t1ud9x7fwPA0RBC/Ad2zs1JWvsNZiQfAQgANgHsBfCQ + 5NUkSb7XK2itzSStSToVx3GnCgJwnOSG936iVqt9SZJkuzLcgQdJrocQzqZp+tI5d1jS/eo85FHBTqdzCMADAKe + NMa+cc/OS7lSjD4MAUAdwG8C5HbggaRXAUhzHxSgI/PqeLUnbzrllSashhBPGmH9CAKhLukDynqQtAMfSNH3zPx + AAfgL2/u9cQzl88QAAAABJRU5ErkJggg==""") + self.ikeywords = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAA + CXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAPRJREFUKJG + lkrFKxEAURc8bHltbWqytYG0TYiC2VnZ+hK3iByhouX7HfkcmhAgigpXFgt02gpXETObZiEVMWI23PecOl+HBPy + Le+5cxaGaLLMsWY1yBHRE5NLO3Hrtwzm0BFEVxJCI3Q2XM7BIIPbYLPAOo6n2M8exHWUROxmaFEJ4AkiRZA+sxb + 1KkLMvrTVKM8SHLsmVVVfMY4ynwnqbplZrZbFPZOacATdOIqs7MrP8/E2Z774+HQAjhMc/zFUBVVfOu6/b7jgLn + Q2VVvQVWXw/ticigNzlS1/X2b+UQwkeapq/f69q2Hb3tgdwBB39ZN5pP4uJac+7GJRAAAAAASUVORK5CYII=""") + self.iwords = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAAA8AAAAICAYAAAAm06XyAAAACXB + IWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAATBJREFUGJWFjy + FLQ2EYhc95t31JEQYiGExaLBODYbvJaFBEGBqHGNW4n6BFBUGw2ZUJCoaxMGT3Xm+1LwkLgrILwzLZdwy7wTD0i + ec9z4EXmEAcx40wDPcn3X5j/xX+gmEYLgA4JLkE4KFSqdzEcdwA0Jc0A0CSzoMgSNrt9pRzrg5gRVLXSNZIfki6 + AnDa6XRKGBsbku5IPpNsJkky55x7AjAv6ZLkV344HJ4VCoUdM9uUNMrlcsuZfBEEwS0ARFG0670/BrDY6/XWq9X + qCEAr75xrABgAeASwKilHElmGbIgkpwF8ZuL45yiKUgCBpG+SLwCOSG5LmvXebwEomdk9yTVJife+ViwWm/1+f9 + tI1iW1ssIrgIGkd0lmZm9mdk1yr1wudwFUzewkTdOU5MEPhImOHsSTJnYAAAAASUVORK5CYII=""") diff --git a/cupcake/texteditor/highlighter.py b/cupcake/texteditor/highlighter.py new file mode 100644 index 0000000..12f4b2b --- /dev/null +++ b/cupcake/texteditor/highlighter.py @@ -0,0 +1,58 @@ +import os, tkinter as tk + +from pygments import lex +from pygments.lexers import get_lexer_for_filename, get_lexer_by_name + + +class Highlighter: + def __init__(self, master, language=None, *args, **kwargs): + self.text = master + self.base = master.base + self.language = language + + if language: + try: + self.lexer = get_lexer_by_name(language) + except: + self.lexer = None + return + else: + try: + self.lexer = get_lexer_for_filename(os.path.basename(master.path), inencoding=master.encoding, encoding=master.encoding) + except: + self.lexer = None + return + + self.setup_highlight_tags() + + def setup_highlight_tags(self): + for token, color in self.base.settings.syntax.items(): + self.text.tag_configure(f"Token.{token}", foreground=color) + + def highlight(self): + if not self.lexer: + return + + for token, _ in self.base.settings.syntax.items(): + self.text.tag_remove(f"Token.{token}", '1.0', tk.END) + + text = self.text.get_all_text() + + # NOTE: Highlighting only visible area + # total_lines = int(self.text.index('end-1c').split('.')[0]) + # start_line = int(self.text.yview()[0] * total_lines) + # first_visible_index = f"{start_line}.0" + # last_visible_index =f"{self.text.winfo_height()}.end" + # for token, _ in self.tag_colors.items(): + # self.text.tag_remove(str(token), first_visible_index, last_visible_index) + # text = self.text.get(first_visible_index, last_visible_index) + + self.text.mark_set("range_start", '1.0') + for token, content in lex(text, self.lexer): + self.text.mark_set("range_end", f"range_start + {len(content)}c") + self.text.tag_add(str(token), "range_start", "range_end") + self.text.mark_set("range_start", "range_end") + + # DEBUG + # print(f"{content} is recognized as a <{str(token)}>") + # print("==================================") diff --git a/cupcake/texteditor/linenumbers/__init__.py b/cupcake/texteditor/linenumbers/__init__.py new file mode 100644 index 0000000..0cd7ae5 --- /dev/null +++ b/cupcake/texteditor/linenumbers/__init__.py @@ -0,0 +1,61 @@ +import tkinter as tk + +from .breakpoint import Breakpoint +from ...utils import Canvas, Menubutton + + +class LineNumbers(Canvas): + def __init__(self, master, text=None, font=None, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.font = font + + self.fg = self.base.theme.linenumbers["foreground"] + self.hfg = self.base.theme.linenumbers["activeforeground"] + + self.config(width=65, bd=0, highlightthickness=0, bg=self.base.theme.linenumbers["background"]) + self.text = text + + def attach(self, text): + self.text = text + + def mark_line(self, line): + dline = self.text.dlineinfo(line) + + if not dline: + return + + y = dline[1] + btn = Menubutton(self, + text=">", font=self.font, cursor="hand2", borderwidth=0, + width=2, height=1, pady=0, padx=0, relief=tk.FLAT, **self.base.theme.linenumbers) + self.create_window(70, y-2, anchor=tk.NE, window=btn) + + def set_bar_width(self, width): + self.configure(width=width) + + def redraw(self, *_): + self.delete(tk.ALL) + + i = self.text.index("@0,0") + while True : + dline = self.text.dlineinfo(i) + if dline is None: + break + + y = dline[1] + linenum = str(i).split(".")[0] + + curline = self.text.dlineinfo(tk.INSERT) + cur_y = curline[1] if curline else None + + self.create_text(40, y, anchor=tk.NE, text=linenum, font=self.font, tag=i, fill=self.hfg if y == cur_y else self.fg) + self.tag_bind(i, "", lambda _, i=i: self.text.select_line(i)) + + # TODO drawing breakpoints - need optimisations + # self.draw_breakpoint(y) + + i = self.text.index(f"{i}+1line") + + def draw_breakpoint(self, y): + bp = Breakpoint(self) + self.create_window(21, y-2, anchor=tk.NE, window=bp) diff --git a/cupcake/editor/linenumbers/breakpoint.py b/cupcake/texteditor/linenumbers/breakpoint.py similarity index 77% rename from cupcake/editor/linenumbers/breakpoint.py rename to cupcake/texteditor/linenumbers/breakpoint.py index 537b48b..1841b3f 100644 --- a/cupcake/editor/linenumbers/breakpoint.py +++ b/cupcake/texteditor/linenumbers/breakpoint.py @@ -5,9 +5,11 @@ class Breakpoint(tk.Label): def __init__(self, master, *args, **kwargs): super().__init__(master, *args, **kwargs) self.master = master + self.base = master.base - self.config(text="โ—", font=("Consolas", 14), bg="#1e1e1e", fg="#1e1e1e", cursor="hand2", - borderwidth=0, width=2, height=1, pady=0, padx=0, relief=tk.FLAT) + self.config(text="โ—", font=("Consolas", 14), fg="#1e1e1e", cursor="hand2", + bg=self.base.theme.linenumbers["background"], borderwidth=0, + width=2, height=1, pady=0, padx=0, relief=tk.FLAT) self.active = False self.hovered = False @@ -15,7 +17,6 @@ def __init__(self, master, *args, **kwargs): def config_bindings(self): self.bind("", self.on_click) - # self.bind("", self.on_click) self.bind("", self.on_enter) self.bind("", self.on_leave) @@ -29,7 +30,6 @@ def redraw(self): else: self.config(fg="#1e1e1e") - def on_click(self, event): self.active = not self.active self.redraw() diff --git a/cupcake/editor/minimap.py b/cupcake/texteditor/minimap.py similarity index 75% rename from cupcake/editor/minimap.py rename to cupcake/texteditor/minimap.py index 2f50ca8..1680dcc 100644 --- a/cupcake/editor/minimap.py +++ b/cupcake/texteditor/minimap.py @@ -1,17 +1,17 @@ import tkinter as tk +from ..utils import Frame -class Minimap(tk.Frame): +#TODO update minimap when scrollbar is used +class Minimap(Frame): def __init__(self, master, textw, *args, **kwargs): super().__init__(master, *args, **kwargs) - self.master = master - self.tw = textw self.font = ("Arial", 1, "bold") - - self.config(bg="#252526", highlightthickness=0) - self.cw = tk.Canvas(self, bg="#1e1e1e", width=100, highlightthickness=0) - self.cw.pack(fill=tk.BOTH, expand=True, side=tk.LEFT) + self.config(highlightthickness=0, bg=self.base.theme.border) + + self.cw = tk.Canvas(self, width=100, highlightthickness=0, **self.base.theme.minimap) + self.cw.pack(fill=tk.BOTH, expand=True, side=tk.LEFT, padx=(1, 0)) self.slider_image = tk.PhotoImage(data="""iVBORw0KGgoAAAANSUhEUgAAAG4AAABFCAYAAACrMNMO AAAACXBIWXMAAABfAAAAXwEqnu0dAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAMBJRE @@ -38,14 +38,17 @@ def attach(self, textw): def redraw(self): self.cw.delete("redrawn") - self.text = self.tw.get_all_text() - self.cw.create_text(5, 0, text=self.text, anchor=tk.NW, font=self.font, fill="#678ca0", tag="redrawn") - - y = int(self.tw.index(tk.INSERT).split(".")[0]) * 2 - self.cw.create_line(0, y, 100, y, fill="#22374b", width=2, tag="redrawn") + self.text = self.tw.get('1.0', tk.END) + self.cw.create_text(5, 0, text=self.text, anchor=tk.NW, font=self.font, fill="grey", tag="redrawn") self.y_bottom_lim = int(self.tw.index(tk.END).split(".")[0]) * 2 + 10 + # self.y_bottom_lim = self.tw.yview()[1] * self.cw.winfo_height() + def redraw_cursor(self): + self.cw.delete("cursor") + y = int(self.tw.index(tk.INSERT).split(".")[0]) * 2 + self.cw.create_line(0, y, 100, y, fill="#dc8c34", width=2, tag="cursor") + def drag_start(self, event): self._drag_data["item"] = self.cw.find_closest(event.x, event.y)[0] self._drag_data["y"] = event.y @@ -69,5 +72,5 @@ def drag(self, event): elif y >= self.y_bottom_lim: self.cw.move("slider", 0, -(y - self.y_bottom_lim)) - self.tw.yview(int(y / self.cw.winfo_height() * 100)) - self.tw.master.redraw_ln() + self.tw.yview(int(y / self.cw.winfo_height() * 350)) + self.tw.master.on_scroll() diff --git a/cupcake/texteditor/syntax.py b/cupcake/texteditor/syntax.py new file mode 100644 index 0000000..74503a3 --- /dev/null +++ b/cupcake/texteditor/syntax.py @@ -0,0 +1,9 @@ +#TODO LSP implementation + +class Syntax: + def __init__(self, master): + self.master = master + self.keywords = [] + + def get_autocomplete_list(self): + return [] diff --git a/cupcake/texteditor/text.py b/cupcake/texteditor/text.py new file mode 100644 index 0000000..9e6e6fe --- /dev/null +++ b/cupcake/texteditor/text.py @@ -0,0 +1,507 @@ +import re, codecs, os +import threading, queue +import tkinter as tk +from collections import deque + +from .syntax import Syntax +from .highlighter import Highlighter +from .autocomplete import AutoComplete + +from ..utils import Text + + +class Text(Text): + def __init__(self, master, path=None, minimalist=False, language=None, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.path = path + self.data = None + self.encoding = 'utf-8' + self.minimalist = minimalist + + self.buffer_size = 1000 + self.bom = True + self.current_word = None + self.words = [] + + if self.path and os.path.isfile(self.path): + self.load_file() + + self.syntax = Syntax(self) + self.auto_completion = AutoComplete( + self, items=self.syntax.get_autocomplete_list()) if not minimalist else None + + self.highlighter = Highlighter(self, language) + + self.focus_set() + self.config_tags() + self.create_proxy() + self.config_bindings() + self.configure(wrap=tk.NONE, relief=tk.FLAT, bg=self.base.theme.background, fg=self.base.theme.foreground) + + self.update_words() + + def config_tags(self): + self.tag_config(tk.SEL, background=self.base.theme.editor.selection) + self.tag_config("highlight", background=self.base.theme.editor.currentword) + self.tag_config("currentline", background=self.base.theme.editor.currentline) + + self.tag_config("found", background=self.base.theme.editor.found) + self.tag_config("foundcurrent", background=self.base.theme.editor.foundcurrent) + + def config_bindings(self): + self.bind("", self.key_release_events) + + self.bind("", self.open_find_replace) + self.bind("", self.multi_selection) + self.bind("", lambda e: self.handle_ctrl_hmovement()) + self.bind("", lambda e: self.handle_ctrl_hmovement(True)) + + self.bind("", self.enter_key_events) + self.bind("", self.tab_key_events) + + if self.minimalist: + return + + self.bind("", self.hide_autocomplete) + self.bind("", self.hide_autocomplete) + self.bind("", self.auto_completion.move_up) + self.bind("", self.auto_completion.move_down) + + # self.bind("", lambda e: self.complete_pair("}")) + # self.bind("", lambda e: self.complete_pair("]")) + # self.bind("", lambda e: self.complete_pair(")")) + + # self.bind("", lambda e: self.surrounding_selection("\'")) + # self.bind("", lambda e: self.surrounding_selection("\"")) + + def key_release_events(self, event): + if event.keysym not in ("Up", "Down", "Return"): + self.show_autocomplete(event) + + match event.keysym: + # autocompletion keys + case "Button-2" | "BackSpace" | "Escape" | "Control_L" | "Control_R" | "space": + self.hide_autocomplete() + case "rightarrow" | "leftarrow": + self.update_completions() + + # bracket pair completions + case "braceleft": + return self.complete_pair("}") + case "bracketleft": + return self.complete_pair("]") + case "parenleft": + return self.complete_pair(")") + + # surroundings for selection + case "apostrophe": + return self.surrounding_selection("\'") + case "quotedbl": + return self.surrounding_selection("\"") + + # extra spaces + case ":" | ",": + self.insert(tk.INSERT, " ") + + case _: + pass + + def enter_key_events(self, *_): + if not self.minimalist and self.auto_completion.active: + self.auto_completion.choose() + return "break" + + return self.check_indentation() + + def tab_key_events(self, *_): + if self.auto_completion.active: + self.auto_completion.choose() + return "break" + + self.insert(tk.INSERT, " "*4) + return "break" + + def get_all_text(self, *args): + return self.get(1.0, tk.END) + + def get_all_text_ac(self, *args): + """ + Helper function for autocomplete.show + extracts all text except the current word. + """ + return self.get(1.0, "insert-1c wordstart-1c") + self.get("insert+1c", tk.END) + + def get_current_word(self): + return self.current_word.strip() + + def update_words(self, *_): + if self.minimalist: + return + + self.words = list(set(re.findall(r"\w+", self.get_all_text_ac()))) + self.after(1000, self.update_words) + + def update_completions(self): + if self.minimalist: + return + + self.auto_completion.update_completions() + + def confirm_autocomplete(self, text): + self.replace_current_word(text) + + def replace_current_word(self, new_word): + if self.current_word.startswith("\n"): + self.delete("insert-1c wordstart+1c", "insert") + else: + self.delete("insert-1c wordstart", "insert") + self.insert("insert", new_word) + + def check_autocomplete_keys(self, event): + """ + Helper function for autocomplete.show to check triggers + """ + return True if event.keysym not in [ + "BackSpace", "Escape", "Return", "Tab", "space", + "Up", "Down", "Control_L", "Control_R"] else False + + def cursor_screen_location(self): + """ + Helper function for autocomplete.show to detect cursor location + """ + pos_x, pos_y = self.winfo_rootx(), self.winfo_rooty() + + cursor = tk.INSERT + bbox = self.bbox(cursor) + if not bbox: + return (0, 0) + + bbx_x, bbx_y, _, bbx_h = bbox + return (pos_x + bbx_x - 1, pos_y + bbx_y + bbx_h) + + def hide_autocomplete(self, *_): + if self.minimalist: + return + + self.auto_completion.hide() + + def show_autocomplete(self, event): + if self.minimalist or not self.check_autocomplete_keys(event): + return + + if self.current_word.strip() not in ["{", "}", ":", "", None, "\""] and not self.current_word.strip()[0].isdigit(): + if not self.auto_completion.active: + if event.keysym in ["Left", "Right"]: + return + pos = self.cursor_screen_location() + self.auto_completion.show(pos) + self.auto_completion.update_completions() + else: + self.auto_completion.update_completions() + else: + if self.auto_completion.active: + self.hide_autocomplete() + + def complete_pair(self, char): + self.insert(tk.INSERT, char) + self.mark_set(tk.INSERT, "insert-1c") + + def surrounding_selection(self, char): + next_char = self.get("insert", "insert+1c") + if next_char == char: + self.mark_set(tk.INSERT, "insert+1c") + self.delete("insert-1c", "insert") + return "break" + + if self.tag_ranges(tk.SEL): + self.insert(char, tk.SEL_LAST) + self.insert(char, tk.SEL_FIRST) + return + + self.complete_pair(char) + return "break" + + def move_to_next_word(self): + self.mark_set(tk.INSERT, self.index("insert+1c wordend")) + + def move_to_previous_word(self): + self.mark_set(tk.INSERT, self.index("insert-1c wordstart")) + + def handle_ctrl_hmovement(self, delta=False): + if delta: + self.move_to_next_word() + else: + self.move_to_previous_word() + + return "break" + + def update_current_indent(self): + line = self.get("insert linestart", "insert lineend") + match = re.match(r'^(\s+)', line) + self.current_indent = len(match.group(0)) if match else 0 + + def update_current_line(self): + self.current_line = self.get("insert linestart", "insert lineend") + return self.current_line + + def add_newline(self, count=1): + self.insert(tk.INSERT, "\n" * count) + + def check_indentation(self, *args): + self.update_current_indent() + if self.update_current_line(): + if self.current_line[-1] in ["{", "[", ":", "("]: + self.current_indent += 4 + elif self.current_line[-1] in ["}", "]", ")"]: + self.current_indent -= 4 + + self.add_newline() + self.insert(tk.INSERT, " " * self.current_indent) + + self.update_current_indent() + + return "break" + + def multi_selection(self, *args): + #TODO: multi cursor editing + return "break" + + def open_find_replace(self, *_): + self.base.findreplace.show(self) + + def detect_encoding(self, file_path): + with open(file_path, 'rb') as file: + bom = file.read(4) + + if bom.startswith(codecs.BOM_UTF8): + return 'utf-8' + elif bom.startswith(codecs.BOM_LE) or bom.startswith(codecs.BOM_BE): + return 'utf-16' + elif bom.startswith(codecs.BOM32_BE) or bom.startswith(codecs.BOM32_LE): + return 'utf-32' + + self.bom = False + return 'utf-8' + + def load_file(self): + try: + encoding = self.detect_encoding(self.path) + file = open(self.path, 'r', encoding=encoding) + self.encoding = encoding + + self.queue = queue.Queue() + threading.Thread(target=self.read_file, args=(file,)).start() + self.process_queue() + except Exception as e: + self.master.unsupported_file() + + def read_file(self, file): + while True: + chunk = file.read(self.buffer_size) + if not chunk: + file.close() + self.queue.put(None) # Signal the end of reading + break + self.queue.put(chunk) + + def process_queue(self): + try: + while True: + chunk = self.queue.get_nowait() + if chunk is None: + self.master.on_change() + self.master.on_scroll() + break + self.write(chunk) + self.update() + self.master.on_scroll() + except queue.Empty: + # If the queue is empty, schedule the next check after a short delay + self.master.after(100, self.process_queue) + + def save_file(self, path=None): + if path: + try: + with open(path, 'w') as fp: + fp.write(self.get_all_text()) + except Exception: + return + + self.path = path + try: + with open(self.path, 'w') as fp: + fp.write(self.get_all_text()) + except Exception: + return + + def copy(self, *_): + self.event_generate("<>") + + def cut(self, *_): + self.event_generate("<>") + + def paste(self, *_): + self.event_generate("<>") + + def set_data(self, data): + self.data = data + + def clear(self): + self.delete(1.0, tk.END) + + def write(self, text, *args): + self.insert(tk.END, text, *args) + + def newline(self, *args): + self.write("\n", *args) + + def get_all_text(self): + return self.get(1.0, tk.END) + + def get_selected_text(self): + try: + return self.selection_get() + except Exception: + return "" + + def add_newline(self, count=1): + self.insert(tk.INSERT, "\n" * count) + + def get_selected_count(self): + return len(self.get_selected_text()) + + @property + def line(self): + return int(self.index(tk.INSERT).split('.')[0]) + + @property + def column(self): + return int(self.index(tk.INSERT).split('.')[1]) + 1 + + @property + def position(self): + lc = self.index(tk.INSERT).split('.') + return [lc[0], int(lc[1]) + 1] + + def scroll_to_end(self): + self.mark_set(tk.INSERT, tk.END) + self.see(tk.INSERT) + + def scroll_to_start(self): + self.mark_set(tk.INSERT, 1.0) + self.see(tk.INSERT) + + def scroll_to_line(self, line): + self.mark_set(tk.INSERT, line) + self.see(tk.INSERT) + + def set_wrap(self, flag=True): + if flag: + self.configure(wrap=tk.WORD) + else: + self.configure(wrap=tk.NONE) + + def set_active(self, flag=True): + if flag: + self.configure(state=tk.NORMAL) + else: + self.configure(state=tk.DISABLED) + + def show_unsupported_dialog(self): + self.set_wrap(True) + self.configure(font=('Arial', 10), padx=10, pady=10) + self.write("This file is not displayed in this editor because it is either binary or uses an unsupported text encoding.") + self.set_active(False) + + def move_cursor(self, position): + self.mark_set(tk.INSERT, position) + + def clear_all_selection(self): + self.tag_remove(tk.SEL, 1.0, tk.END) + + def highlight_current_line(self, *_): + self.tag_remove("currentline", 1.0, tk.END) + if self.get_selected_text(): + return + + line = int(self.index(tk.INSERT).split(".")[0]) + start = str(float(line)) + end = str(float(line + 1)) + self.tag_add("currentline", start, end) + + def select_line(self, line): + self.clear_all_selection() + + line = int(line.split(".")[0]) + start = str(float(line)) + end = str(float(line + 1)) + self.tag_add(tk.SEL, start, end) + + self.move_cursor(end) + + def highlight_current_word(self): + if self.minimalist or self.get_selected_text(): + return + + self.tag_remove("highlight", 1.0, tk.END) + word = re.findall(r"\w+", self.get("insert wordstart", "insert wordend")) + if any(word) and word[0] not in self.syntax.keywords: + self.highlight_pattern(f"\\y{word[0]}\\y", "highlight", regexp=True) + + # elif word := self.get_selected_text(): + # self.highlight_pattern(word, "highlight", end="sel.first") + # self.highlight_pattern(word, start="sel.last") + + + def highlight_pattern(self, pattern, tag, start="1.0", end=tk.END, regexp=False): + start = self.index(start) + end = self.index(end) + + self.mark_set("matchStart", start) + self.mark_set("matchEnd", start) + self.mark_set("searchLimit", end) + + self.tag_remove(tag, start, end) + + count = tk.IntVar() + while True: + index = self.search(pattern, "matchEnd", "searchLimit", count=count, regexp=regexp) + if index == "" or count.get() == 0: + break + + self.mark_set("matchStart", index) + self.mark_set("matchEnd", f"{index}+{count.get()}c") + + self.tag_add(tag, "matchStart", "matchEnd") + + def refresh(self, *args): + if self.minimalist: + return + + self.current_word = self.get("insert-1c wordstart", "insert") + self.highlighter.highlight() + self.highlight_current_line() + self.highlight_current_word() + + def create_proxy(self): + self._orig = self._w + "_orig" + self.tk.call("rename", self._w, self._orig) + self.tk.createcommand(self._w, self._proxy) + + def _proxy(self, *args): + if args[0] == 'get' and (args[1] == tk.SEL_FIRST and args[2] == tk.SEL_LAST) and not self.tag_ranges(tk.SEL): + return + if args[0] == 'delete' and (args[1] == tk.SEL_FIRST and args[2] == tk.SEL_LAST) and not self.tag_ranges(tk.SEL): + return + + cmd = (self._orig,) + args + result = self.tk.call(cmd) + + if (args[0] in ("insert", "replace", "delete") or args[0:3] == ("mark", "set", "insert")): + self.event_generate("<>", when="tail") + + elif (args[0:2] == ("xview", "moveto") or args[0:2] == ("yview", "moveto") or + args[0:2] == ("xview", "scroll") or args[0:2] == ("yview", "scroll")): + self.event_generate("<>", when="tail") + + return result diff --git a/cupcake/utils/__init__.py b/cupcake/utils/__init__.py new file mode 100644 index 0000000..d1bd8b5 --- /dev/null +++ b/cupcake/utils/__init__.py @@ -0,0 +1,19 @@ +""" +Various types of widgets/functions used across the editor +""" + +from .directorytree import DirectoryTree +from .canvas import Canvas +from .frame import Frame +from .filetype import FileType +from .label import Label +from .scrollbar import Scrollbar +from .scrollableframe import ScrollableFrame +from .tree import Tree +from .menubutton import Menubutton +from .toplevel import Toplevel +from .text import Text + +@staticmethod +def clamp(value, min_val, max_val): + return min(max(min_val, value), max_val) diff --git a/cupcake/utils/canvas.py b/cupcake/utils/canvas.py new file mode 100644 index 0000000..c068451 --- /dev/null +++ b/cupcake/utils/canvas.py @@ -0,0 +1,8 @@ +import tkinter as tk + + +class Canvas(tk.Canvas): + def __init__(self, master, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.master = master + self.base = master.base diff --git a/cupcake/utils/directorytree.py b/cupcake/utils/directorytree.py new file mode 100644 index 0000000..0962b90 --- /dev/null +++ b/cupcake/utils/directorytree.py @@ -0,0 +1,96 @@ +import os +import tkinter as tk + +from .frame import Frame +from .tree import Tree + + +class DirectoryTree(Frame): + def __init__(self, master, path=None, openfile=None, preview_file=None, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.openfile = openfile + self.preview_file = preview_file + + self.nodes = {} + + self.ignore_dirs = [".git", "__pycache__"] + self.ignore_exts = [".pyc"] + + self.tree = Tree(self, path, doubleclick=self.openfile, singleclick=self.preview_file, *args, **kwargs) + self.tree.grid(row=0, column=0, sticky=tk.NSEW) + + self.path = path + if path: + self.change_path(path) + else: + self.tree.insert('', 0, text='You have not yet opened a file.') + self.tree.tree.bind("<>", self.on_treeview_open) + + def change_path(self, path): + self.nodes.clear() + self.path = path + if self.path: + self.tree.clear_tree() + self.create_root() + else: + self.tree.clear_tree() + self.tree.insert('', 0, text='Empty') + + def create_root(self): + self.update_treeview([(p, os.path.join(self.path, p)) for p in os.listdir(self.path)]) + + for path, item in list(self.nodes.items()): + if not os.path.exists(path): + self.tree.delete(item) + self.nodes.pop(path) + + def get_actionset(self): + return self.actionset + + def scandir(self, path): + entries = [] + for entry in os.scandir(path): + entries.append((entry.name, entry.path)) + return entries + + def update_treeview(self, entries, parent=""): + entries.sort(key=lambda x: (not os.path.isdir(x[1]), x[0])) + for name, path in entries: + if os.path.isdir(path): + if name in self.ignore_dirs: + continue + + item = self.tree.tree.insert(parent, "end", text=f" {name}", values=[path, 'directory'], image='foldericon', open=False) + self.tree.tree.insert(item, "end", text="loading...") + self.tree.tree.update_idletasks() + else: + if name.split(".")[-1] in self.ignore_exts: + continue + + #TODO check filetype and get matching icon, cases + item = self.tree.tree.insert(parent, "end", text=f" {name}", values=[path, 'file'], image='document') + self.tree.tree.update_idletasks() + + def on_treeview_open(self, event): + item_id = self.tree.tree.focus() + if not item_id: + return + + path = self.tree.tree.item(item_id, "values")[0] + if not path or not os.path.isdir(path): + return + + parent_id = item_id + self.tree.tree.delete(*self.tree.tree.get_children(parent_id)) + self.update_treeview(self.scandir(path), parent_id) + + def close_directory(self): + self.change_path(None) + + def openfile(self, _): + if self.openfile: + self.openfile() + + def preview_file(self, *_): + if self.preview_file: + self.preview_file() diff --git a/cupcake/utils/filetype.py b/cupcake/utils/filetype.py new file mode 100644 index 0000000..1c81ede --- /dev/null +++ b/cupcake/utils/filetype.py @@ -0,0 +1,11 @@ +import filetype + + +class FileType: + @staticmethod + def get_file_type(file_path): + return filetype.guess(file_path) + + @staticmethod + def is_image(file_path): + return filetype.is_image(file_path) diff --git a/cupcake/utils/frame.py b/cupcake/utils/frame.py new file mode 100644 index 0000000..12a0760 --- /dev/null +++ b/cupcake/utils/frame.py @@ -0,0 +1,11 @@ +import tkinter as tk + + +class Frame(tk.Frame): + def __init__(self, master, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.master = master + try: + self.base = master.base + except: + self.base = self diff --git a/cupcake/utils/label.py b/cupcake/utils/label.py new file mode 100644 index 0000000..32fa0e8 --- /dev/null +++ b/cupcake/utils/label.py @@ -0,0 +1,8 @@ +import tkinter as tk + + +class Label(tk.Label): + def __init__(self, master, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.master = master + self.base = master.base diff --git a/cupcake/utils/menubutton.py b/cupcake/utils/menubutton.py new file mode 100644 index 0000000..c3eed12 --- /dev/null +++ b/cupcake/utils/menubutton.py @@ -0,0 +1,8 @@ +import tkinter as tk + + +class Menubutton(tk.Menubutton): + def __init__(self, master, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.master = master + self.base = master.base diff --git a/cupcake/utils/scrollableframe.py b/cupcake/utils/scrollableframe.py new file mode 100644 index 0000000..476c872 --- /dev/null +++ b/cupcake/utils/scrollableframe.py @@ -0,0 +1,44 @@ +import tkinter as tk +from tkinter import ttk + +from .frame import Frame +from .canvas import Canvas + + +class Scrollbar(ttk.Scrollbar): + def set(self, low, high): + if float(low) <= 0.0 and float(high) >= 1.0: + self.pack_forget() + else: + self.pack(side=tk.RIGHT, fill=tk.Y) + ttk.Scrollbar.set(self, low, high) + + +class ScrollableFrame(Frame): + def __init__(self, master, *args, **kwargs): + super().__init__(master, *args, **kwargs) + + self.scrollbar = Scrollbar(self) + self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.canvas = Canvas(self, yscrollcommand=self.scrollbar.set, bg=self.base.theme.background) + self.canvas.configure(highlightthickness=0) + self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + self.scrollbar.config(command=self.canvas.yview) + + self.content = Frame(self.canvas, bg=self.base.theme.background) + self._content = self.canvas.create_window((0, 0), window=self.content, anchor="nw") + + self.content.bind("", self._scroll) + self.canvas.bind("", self._configure_canvas) + + def _scroll(self, event): + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + + def _configure_canvas(self, event): + canvas_width = event.width + self.canvas.itemconfig(self._content, width=canvas_width) + + def add(self, content): + content.pack(in_=self.content) diff --git a/cupcake/utils/scrollbar.py b/cupcake/utils/scrollbar.py new file mode 100644 index 0000000..5a6bdf8 --- /dev/null +++ b/cupcake/utils/scrollbar.py @@ -0,0 +1,10 @@ +from tkinter import ttk + + +class Scrollbar(ttk.Scrollbar): + def set(self, low, high): + if float(low) <= 0.0 and float(high) >= 1.0: + self.grid_remove() + else: + self.grid() + ttk.Scrollbar.set(self, low, high) diff --git a/cupcake/utils/text.py b/cupcake/utils/text.py new file mode 100644 index 0000000..f650bff --- /dev/null +++ b/cupcake/utils/text.py @@ -0,0 +1,11 @@ +import tkinter as tk + + +class Text(tk.Text): + """ + normal text with reference to base + """ + def __init__(self, master, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.master = master + self.base = master.base diff --git a/cupcake/utils/toplevel.py b/cupcake/utils/toplevel.py new file mode 100644 index 0000000..c5725df --- /dev/null +++ b/cupcake/utils/toplevel.py @@ -0,0 +1,8 @@ +import tkinter as tk + + +class Toplevel(tk.Toplevel): + def __init__(self, master, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.master = master + self.base = master.base diff --git a/cupcake/utils/tree.py b/cupcake/utils/tree.py new file mode 100644 index 0000000..bc0c7ba --- /dev/null +++ b/cupcake/utils/tree.py @@ -0,0 +1,88 @@ +import tkinter.ttk as ttk +import tkinter as tk + +from .scrollbar import Scrollbar +from .frame import Frame + + +class Tree(Frame): + def __init__(self, master, path=None, doubleclick=lambda _: None, singleclick=lambda _: None, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.config(bg=self.base.theme.tree["background"]) + + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + + self.path = path + self.doubleclick = doubleclick + self.singleclick = singleclick + + self.tree = ttk.Treeview(self, show="tree", columns=("fullpath", "type"), + displaycolumns='', selectmode=tk.BROWSE) + self.tree.grid(row=0, column=0, sticky=tk.NSEW) + + self.scrollbar = Scrollbar(self, orient=tk.VERTICAL, command=self.tree.yview, style="TreeScrollbar") + self.scrollbar.grid(row=0, column=1, sticky=tk.NS) + + self.tree.config(yscrollcommand=self.scrollbar.set) + self.scrollbar.config(command=self.tree.yview) + + self.tree.bind("", self.doubleclick) + self.tree.bind("<>", self.check_singleclick) + + def check_singleclick(self, _): + if self.item_type(self.focus()) == 'file': + if self.singleclick: + self.singleclick(self.item_fullpath(self.focus())) + else: + self.toggle_node(self.focus()) + + def clear_node(self, node): + self.tree.delete(*self.tree.get_children(node)) + + def clear_tree(self): + self.clear_node('') + + def collapse_all(self): + for node in self.tree.get_children(): + self.tree.item(node, open=False) + + def delete(self, *a, **kw): + self.tree.delete(*a, *kw) + + def focus(self): + return self.tree.focus() + + def get_children(self, *a, **kw): + return self.tree.get_children(*a, **kw) + + def insert(self, *args, **kwargs): + return self.tree.insert(*args, **kwargs) + + def is_open(self, node): + return self.tree.item(node, "open") + + def item(self, *a, **kw): + return self.tree.item(*a, **kw) + + def item_type(self, item): + return self.set(item, "type") + + def item_fullpath(self, item): + return self.set(item, "fullpath") + + def selected_path(self): + return self.item_fullpath(self.focus()) + + def selected_type(self): + return self.item_type(self.focus()) + + def set(self, *args, **kwargs): + return self.tree.set(*args, **kwargs) + + def toggle_node(self, node): + if self.item_type(node) == 'directory': + if self.is_open(node): + self.tree.item(node, open=False) + else: + self.tree.item(node, open=True) diff --git a/examples/__init__.py b/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/examples/basic.py b/examples/basic.py index d995b4c..078ab37 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -1,19 +1,23 @@ """ -Cupcake running with C++ code editing features. +Cupcake running with code editing features. Features enabled: - Syntax highlighting -- Autocompletions (keywords & words) +- Autocompletions (words only, no lsp) - Minimap """ +import sys +from os.path import abspath, dirname, join +sys.path.append(abspath(join(dirname(__file__), '..'))) + import tkinter as tk from cupcake import Editor root = tk.Tk() root.minsize(800, 600) -e = Editor(root) +e = Editor(root, __file__) e.pack(expand=1, fill=tk.BOTH) root.mainloop() diff --git a/examples/diff.py b/examples/diff.py new file mode 100644 index 0000000..a8f22a8 --- /dev/null +++ b/examples/diff.py @@ -0,0 +1,50 @@ +""" +Cupcake diff editor +""" + +import sys +from os.path import abspath, dirname, join +sys.path.append(abspath(join(dirname(__file__), '..'))) + +import tkinter as tk +from cupcake import Editor + +root = tk.Tk() +root.minsize(800, 600) + +data1 = """import sys +from os.path import abspath, dirname, join +sys.path.append(abspath(join(dirname(__file__), '..'))) + +import tkinter as tk +from cupcake import Editor + +root = tk.Tk() +root.minsize(800, 600) + +e = Editor(root, __file__, darkmode=False) +e.pack(expand=1, fill=tk.BOTH) + +root.mainloop() +""" + +data2 = """import sys +from os.path import abspath, dirname, join +sys.path.append(abspath(join(dirname(__file__), '..'))) + +import tkinter as tk +from cupcake import Editor, Languages + +root = tk.Tk() +root.minsize(800, 600) +# this is a new line +e = Editor(root, __file__, darkmode=False) + +root.mainloop() +""" + +e = Editor(root, diff=True) +e.content.show_diff_text(data1, data2) +e.pack(expand=1, fill=tk.BOTH) + +root.mainloop() diff --git a/examples/hello.py b/examples/hello.py deleted file mode 100644 index 6b41166..0000000 --- a/examples/hello.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Hello, have a Cupcake! ๐Ÿง -""" - -import tkinter as tk -from cupcake import Editor - -root = tk.Tk() -root.minsize(700, 600) - -e = Editor(root) -e.pack(expand=1, fill=tk.BOTH) - -e.insert("Hello, have a cupcake!") - -root.mainloop() diff --git a/examples/image.py b/examples/image.py new file mode 100644 index 0000000..8bf05a2 --- /dev/null +++ b/examples/image.py @@ -0,0 +1,18 @@ +""" +Cupcake with TypeScript lexer +""" + +import sys +from os.path import abspath, dirname, join +sys.path.append(abspath(join(dirname(__file__), '..'))) + +import tkinter as tk +from cupcake import Editor + +root = tk.Tk() +root.minsize(800, 600) + +e = Editor(root, ".github/res/screenshot.png") +e.pack(expand=1, fill=tk.BOTH) + +root.mainloop() diff --git a/examples/light.py b/examples/light.py new file mode 100644 index 0000000..f4709e0 --- /dev/null +++ b/examples/light.py @@ -0,0 +1,18 @@ +""" +Cupcake light mode +""" + +import sys +from os.path import abspath, dirname, join +sys.path.append(abspath(join(dirname(__file__), '..'))) + +import tkinter as tk +from cupcake import Editor + +root = tk.Tk() +root.minsize(800, 600) + +e = Editor(root, __file__, darkmode=False) +e.pack(expand=1, fill=tk.BOTH) + +root.mainloop() diff --git a/examples/samples/fizzbuzz.cpp b/examples/samples/fizzbuzz.cpp deleted file mode 100644 index e66e76a..0000000 --- a/examples/samples/fizzbuzz.cpp +++ /dev/null @@ -1,19 +0,0 @@ -// try opening in cupcake -#include - -int main(void) -{ - int i; - for (i=1; i<=100; i++) - { - if (i%15 == 0) - printf ("FizzBuzz\t"); - else if ((i%3) == 0) - printf("Fizz\t"); - else if ((i%5) == 0) - printf("Buzz\t"); - else - printf("%d\t", i); - } - return 0; -} diff --git a/examples/samples/hello.cpp b/examples/samples/hello.cpp deleted file mode 100644 index 9c2a6f7..0000000 --- a/examples/samples/hello.cpp +++ /dev/null @@ -1,8 +0,0 @@ -// try opening in cupcake -#include -using namespace std; - -int main() { - cout << "Hello, world!" << endl; - return 0; -} diff --git a/examples/typescript.py b/examples/typescript.py new file mode 100644 index 0000000..2362523 --- /dev/null +++ b/examples/typescript.py @@ -0,0 +1,29 @@ +""" +Cupcake with TypeScript lexer +""" + +import sys +from os.path import abspath, dirname, join +sys.path.append(abspath(join(dirname(__file__), '..'))) + +import tkinter as tk +from cupcake import Editor, Languages + +root = tk.Tk() +root.minsize(800, 600) + +e = Editor(root, language=Languages.TYPESCRIPT) +e.pack(expand=1, fill=tk.BOTH) + +e.content.insert("end", """import "./theme.scss"; +import "./global.css"; +import App from './App.svelte'; + +const app = new App({ + target: document.body +}); + +export default app; +""") + +root.mainloop() diff --git a/pyproject.toml b/pyproject.toml index d1abb80..8448e5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,11 @@ [tool.poetry] -name = "cupcake-editor" -version = "0.5.1" -description = "Embeddable code editor for tkinter" +name = "cupcake" +version = "0.25.0" +description = "Embeddable code editor for python apps" authors = ["Billy "] +license = "MIT" readme = "README.md" + packages = [ { include = "cupcake" } ] @@ -11,9 +13,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" -[tool.poetry.dev-dependencies] -pytest = "^5.2" [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt index e69de29..4b7266c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,4 @@ +pygments +filetype +pillow +toml diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_cupcake.py b/tests/test_cupcake.py index 1902002..56712fc 100644 --- a/tests/test_cupcake.py +++ b/tests/test_cupcake.py @@ -1,5 +1,10 @@ +import sys +from os.path import abspath, dirname, join +sys.path.append(abspath(join(dirname(__file__), '..'))) + + from cupcake import __version__ def test_version(): - assert __version__ == '0.5.1' + assert __version__ == '0.25.0' diff --git a/tests/test_editor.py b/tests/test_editor.py deleted file mode 100644 index 0cbaadc..0000000 --- a/tests/test_editor.py +++ /dev/null @@ -1,12 +0,0 @@ -import tkinter as tk -from cupcake import Editor - - -def test_editor(): - root = tk.Tk() - root.after(10, root.destroy) - - e = Editor(root) - e.pack() - - root.mainloop()