├── .gitignore
├── img
├── 1.png
├── 10.png
├── 11.png
├── 12.png
├── 13.png
├── 14.png
├── 15.png
├── 16.png
├── 17.png
├── 18.png
├── 19.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
└── 9.png
├── Main.sublime-menu
├── Rainbowth.sublime-settings
├── Setup-Instructions.md
├── README.md
└── rainbowth.py
/.gitignore:
--------------------------------------------------------------------------------
1 | Rainbowth.cache
2 |
--------------------------------------------------------------------------------
/img/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/1.png
--------------------------------------------------------------------------------
/img/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/10.png
--------------------------------------------------------------------------------
/img/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/11.png
--------------------------------------------------------------------------------
/img/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/12.png
--------------------------------------------------------------------------------
/img/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/13.png
--------------------------------------------------------------------------------
/img/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/14.png
--------------------------------------------------------------------------------
/img/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/15.png
--------------------------------------------------------------------------------
/img/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/16.png
--------------------------------------------------------------------------------
/img/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/17.png
--------------------------------------------------------------------------------
/img/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/18.png
--------------------------------------------------------------------------------
/img/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/19.png
--------------------------------------------------------------------------------
/img/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/2.png
--------------------------------------------------------------------------------
/img/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/3.png
--------------------------------------------------------------------------------
/img/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/4.png
--------------------------------------------------------------------------------
/img/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/5.png
--------------------------------------------------------------------------------
/img/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/6.png
--------------------------------------------------------------------------------
/img/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/7.png
--------------------------------------------------------------------------------
/img/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/8.png
--------------------------------------------------------------------------------
/img/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitequark/rainbowth/HEAD/img/9.png
--------------------------------------------------------------------------------
/Main.sublime-menu:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "preferences",
4 | "children": [
5 | {
6 | "id": "package-settings",
7 | "children": [
8 | {
9 | // "caption": "Rainbowth",
10 | // "children": [
11 | // {
12 | // "caption": "Settings",
13 | // "command": "edit_settings",
14 | // "args": {
15 | // "base_file": "${packages}/Rainbowth/Rainbowth.sublime-settings",
16 | // "default": "// Any settings in this file overrides the default ones\n{\n\t$0\n}\n"
17 | // }
18 | // }
19 | // ]
20 | "caption": "Rainbowth",
21 | "command": "edit_settings",
22 | "args": {
23 | "base_file": "${packages}/Rainbowth/Rainbowth.sublime-settings",
24 | "default": "// Any settings in this file overrides the default ones\n{\n\t$0\n}\n"
25 | }
26 | }
27 | ]
28 | }
29 | ]
30 | }
31 | ]
--------------------------------------------------------------------------------
/Rainbowth.sublime-settings:
--------------------------------------------------------------------------------
1 | {
2 | // Do not change these settings
3 | // Change the user settings (Packages/User/Rainbowth.sublime-settings) instead.
4 | //
5 | // If it does not work yet:
6 | // Extract your theme to your `Package` folder.
7 | // Instructions here: https://github.com/whitequark/rainbowth#setup
8 | //
9 |
10 | "languages": ["lisp", "scheme", "clojure", "clojurescript", "hylang"],
11 |
12 | "palettes": {
13 | "default": ["red", "orange", "yellow", "green", "blue", "indigo", "violet"],
14 | "Tomorrow Night": ["#c66", "#de935f", "#ee6", "#b5bd68", "#81a2be", "#b294bb", "#ff69b4", "#8a2be2"],
15 | "Monokai": ["#AE81FF", "#66D9EF", "#A6E22E", "#FD971F", "#F92672"]
16 | },
17 |
18 | // Toggle whether languages from list are excluded or included. Default: false
19 | "exclude_languages": false,
20 |
21 | // Toggle whether strings should be ignored. Default: false
22 | "disable_inside_string": false,
23 |
24 | // Toggle whether comments should be ignored. Default: false
25 | "disable_inside_comment": false,
26 |
27 | // Enables custom pre- and suffixes. Currently only single character 'fixes.
28 | // Note: Characters cannot appear in both the prefix and the suffix (Which only makes sense, doesn't it?)
29 | // Default prefix and suffix are just () and [].
30 | "custom_signs":
31 | {
32 | "enabled": false,
33 | "prefix": "({[",
34 | "suffix": ")}]"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Setup-Instructions.md:
--------------------------------------------------------------------------------
1 | ## Starting
2 | This is a sample code being used to show what the setup process is like. At this moment, nothing is set up.
3 | 
4 |
5 | ## Install package control
6 | If you do not have package control installed, install it from the `Command Palette` (`Cmd`+`Shift`+`p` on macOS, `Ctrl`+`Shift`+`p` elsewhere):
7 | 
8 |
9 | ## Install Rainbowth package
10 | Using the command palette, install the `Rainbowth` package:
11 | 
12 | 
13 |
14 | ## Browse packages
15 | You can visit your packages by going to *Preferences > Browse Packages...*:
16 | 
17 | 
18 | 
19 |
20 | ## Install a color theme
21 | For our example, we are going to install the *Dracula theme*.
22 | 
23 | 
24 |
25 | ## Set to the installed theme
26 | 
27 | 
28 |
29 | ## Install PackageResourceViewer
30 | In order for Rainbowth to be able to edit the themes to color the text, we will have to create the theme files in the *Packages* folder. This can be easily done using a package called **PackageResourceViewer**.
31 | 
32 | 
33 |
34 | ## Extract theme files
35 | Using PackageResourceViewer, extract the relevant files for the theme (in our case, the *Dracula* theme):
36 | 
37 | 
38 |
39 | This will create the files related to the theme in *Packages* folder. The folder should look somewhat like this now:
40 | 
41 |
42 | ## Rainbowth.sublime-settings
43 | Create a `Rainbowth.sublime-settings` in `User`:
44 | 
45 |
46 | Now, edit the file with languages and colors you want to use:
47 | 
48 |
49 | ## Success
50 | The changes should apply automatically, if not, quit Sublime and open it again to see Rainbowth working.
51 | 
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Rainbowth
2 | =========
3 |
4 | ### What is this?
5 |
6 | Rainbowth is a Sublime Text 3 plugin that automagically highlights matching parentheses, brackets, and curly braces in source code. While the name does imply a certain sequence of colors, the palette used to paint them is entirely configurable; nonetheless, the effect is perhaps best demonstrated when viewed with a theme like [Tomorrow Night](https://github.com/chriskempson/tomorrow-theme/tree/master/textmate):
7 |
8 | 
9 |
10 | ### Why?
11 |
12 | It took about three hours for Racket to grow on me. I know with a fair amount of certainty that I'd like to master it, but the structure is going to take some getting used to. This plugin attempts to overcome one of the primary barriers to entry, that of not being able to tell what's related beyond the matching of a single parenthetical pair.
13 |
14 | ### Installation
15 |
16 | Install Rainbowth via [Package Control](https://packagecontrol.io/packages/Rainbowth), or clone this repository directly into your Packages directory.
17 |
18 | ### Setup
19 |
20 | The color scheme must be unpacked in the user's `Packages` folder and writable for Rainbowth to be able to automatically insert its palette.
21 |
22 | The default themes can be located in the `Color Scheme - Default.sublime-package` (or `Color Scheme - Legacy.sublime-package` on newer Sublime Text installations) file in the Sublime Text installation folder. Use a zip archive manager to extract the `.tmTheme` file corresponding to the theme you like. There is currently no support for the new `.sublime-color-scheme` file format.
23 |
24 | * [Step-by-step setup instructions](Setup-Instructions.md)
25 |
26 | ### Configuration
27 |
28 | Rainbowth's default configuration can be customized by creating a `Rainbowth.sublime-settings` file in `Packages/User`.
29 |
30 | The `palettes` setting is a mapping of theme names to the list of colors to use for painting parentheses while using that theme, outermost first. When using a theme not specified, the default ROYGBIV sequence will be used.
31 |
32 | ```
33 | {
34 | "palettes": {
35 | "default": ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]
36 | }
37 | }
38 | ```
39 |
40 | The `languages` setting is list of languages included (or excluded if `exclude_languages` setting is set to true) in highlighting.
41 |
42 | ```
43 | {
44 | "language_mode": "include",
45 | "languages": ["lisp", "scheme", "clojure", "clojurescript", "hylang"]
46 | }
47 | ```
48 |
49 | ### Contributing
50 |
51 | Comments, criticisms, and code are all eagerly welcomed.
52 |
--------------------------------------------------------------------------------
/rainbowth.py:
--------------------------------------------------------------------------------
1 | import sublime, sublime_plugin, re, json, os, codecs
2 | from collections import defaultdict
3 |
4 | class ViewInfo:
5 | def __init__(self, color_count, per_line_depths):
6 | self.keys = ['rainbowth{}'.format(index) for index in range(color_count)]
7 | self.keys_lineHighlight = [scope + '-lineHighlight' for scope in self.keys]
8 | self.per_line_depths = per_line_depths
9 |
10 | self.prepared_regions = {}
11 | for key in self.keys + self.keys_lineHighlight:
12 | self.prepared_regions[key] = []
13 |
14 | for line in self.per_line_depths:
15 | depths = self.per_line_depths[line]
16 | for depth, regions in enumerate(depths):
17 | self.prepared_regions[self.keys[depth]] += regions
18 |
19 | def update(self, old_highlighted_line, new_highlighted_line):
20 | # Sublime does not permit us to set foreground color of a region
21 | # without simultaneously filling its background, so we do this
22 | # gross workaround: paint the background to the color it would have
23 | # been otherwise. Since selection erases foreground colors of regions
24 | # as well, we do this only for regular and current line backgrounds.
25 |
26 | if old_highlighted_line is not None:
27 | depths = self.per_line_depths[old_highlighted_line]
28 | for depth, regions in enumerate(depths):
29 | visible_regions = self.prepared_regions[self.keys_lineHighlight[depth]]
30 | for region in regions:
31 | visible_regions.remove(region)
32 | self.prepared_regions[self.keys[depth]] += regions
33 |
34 | if new_highlighted_line is not None:
35 | depths = self.per_line_depths[new_highlighted_line]
36 | for depth, regions in enumerate(depths):
37 | visible_regions = self.prepared_regions[self.keys[depth]]
38 | for region in regions:
39 | if region in visible_regions: # FIXME: should be always true
40 | visible_regions.remove(region)
41 | self.prepared_regions[self.keys_lineHighlight[depth]] += regions
42 |
43 | def highlight(self, view):
44 | for key in self.prepared_regions:
45 | view.erase_regions(key)
46 | view.add_regions(key, self.prepared_regions[key],
47 | scope=key, flags=sublime.DRAW_NO_OUTLINE)
48 |
49 | class Rainbowth(sublime_plugin.EventListener):
50 | @staticmethod
51 | def cache_file_path():
52 | result = os.path.join(sublime.cache_path(), 'Rainbowth', 'Rainbowth.cache')
53 | if not os.path.exists(os.path.dirname(result)):
54 | os.makedirs(os.path.dirname(result))
55 | return result
56 |
57 | def __init__(self):
58 | self.cache = None
59 | self.view_infos = {}
60 |
61 | def read_cache(self):
62 | if self.cache is None:
63 | if os.path.exists(self.cache_file_path()):
64 | with codecs.open(self.cache_file_path(), 'r', 'utf-8') as cache_file:
65 | self.cache = json.load(cache_file)
66 | else:
67 | self.cache = {}
68 |
69 | def write_cache(self):
70 | with codecs.open(self.cache_file_path(), 'w', 'utf-8') as cache_file:
71 | json.dump(self.cache, cache_file)
72 |
73 | def current_color_scheme(self, view):
74 | """Returns (absolute color scheme path, scheme name)."""
75 | scheme_relative_path = view.settings().get('color_scheme')
76 | scheme_name, _ = os.path.splitext(os.path.basename(scheme_relative_path))
77 | sublime_base_path = os.path.dirname(sublime.packages_path())
78 | scheme_path = os.path.join(sublime_base_path, scheme_relative_path)
79 | return scheme_path, scheme_name
80 |
81 | def perturb_color(self, color):
82 | """
83 | Apply a minimal change to `color`, which must be in CSS hex format,
84 | and return the modified color.
85 | """
86 | if color[0] == '#' and len(color) in (4, 7, 9):
87 | # Normalize to 8-digit hex.
88 | color = re.sub('^#(.)(.)(.)$', r'#\1\1\2\2\3\3', color)
89 | color = re.sub('^#(......)$', r'#\1ff', color)
90 |
91 | # Perturb.
92 | color_value = int(color[1:], base=16)
93 | if color_value & 0xff00 == 0xff00:
94 | color_value -= 0x100
95 | else:
96 | color_value += 0x100
97 | color = "#{:08x}".format(color_value)
98 |
99 | return color
100 |
101 | def get_setting(self, scheme_xml, setting):
102 | # This is an awful hack, but even with a full-blown XML parser,
103 | # parsing .tmTheme files is painful. Feel free to send a PR.
104 | settings = re.search('settings\s*(.+?)',
105 | scheme_xml, flags=re.DOTALL).group(1)
106 | value = re.search('{}\s*(.+?)'.format(setting),
107 | settings, flags=re.DOTALL).group(1)
108 | return value
109 |
110 | def colors_to_xml(self, colors, background, lineHighlight):
111 | xml = []
112 |
113 | # If we don't perturb the background color, ST treats it as absent
114 | # and refuses to change foreground color of the letters either.
115 | # Instead, it uses the foreground color to draw the outline and fill.
116 | background = self.perturb_color(background)
117 |
118 | for index, color in enumerate(colors):
119 | xml.append(
120 | ''
121 | 'scoperainbowth{index}'
122 | 'settings'
123 | 'foreground{foreground}'
124 | 'background{background}'
125 | ''
126 | ''
127 | ''
128 | 'scoperainbowth{index}-lineHighlight'
129 | 'settings'
130 | 'foreground{foreground}'
131 | 'background{lineHighlight}'
132 | ''
133 | ''
134 | .format(index=index, foreground=color,
135 | background=background, lineHighlight=lineHighlight))
136 |
137 | return "".join(xml)
138 |
139 | def update_color_scheme(self, view):
140 | scheme_path, scheme_name = self.current_color_scheme(view)
141 |
142 | settings = sublime.load_settings('Rainbowth.sublime-settings')
143 | palettes = settings.get('palettes')
144 | colors = palettes.get(scheme_name, palettes['default'])
145 |
146 | self.read_cache()
147 | if colors == self.cache.get(scheme_name, None):
148 | # Already updated.
149 | return colors
150 |
151 | print("Rainbowth: color scheme needs updating")
152 |
153 | # Not updated; do it!
154 | with codecs.open(scheme_path, 'r', 'utf-8') as scheme_file:
155 | scheme_xml = scheme_file.read()
156 |
157 | background = self.get_setting(scheme_xml, 'background')
158 | lineHighlight = self.get_setting(scheme_xml, 'lineHighlight')
159 |
160 | # Cut out our old colors, if any.
161 | scheme_xml = re.sub('[ \t]+.+\n', '', scheme_xml)
162 |
163 | # Insert our updated colors.
164 | rainbowth = '\t{}'. \
165 | format(self.colors_to_xml(colors, background, lineHighlight))
166 | scheme_xml = re.sub('', rainbowth + '\n\t', scheme_xml)
167 |
168 | with codecs.open(scheme_path, 'w', 'utf-8') as scheme_file:
169 | scheme_file.write(scheme_xml)
170 |
171 | self.cache[scheme_name] = colors
172 | self.write_cache()
173 |
174 | return colors
175 |
176 | def is_written_in(self, view, languages):
177 | scope_names = view.scope_name(0).split(' ')
178 | for language in [scope.format(lang) for lang in languages
179 | for scope in ["source.{}", "text.{}", "text.tex.{}"]]:
180 | if language in scope_names:
181 | return True
182 | return False
183 |
184 | def on_activated_async(self, view):
185 | settings = sublime.load_settings('Rainbowth.sublime-settings')
186 | languages = settings.get('languages')
187 | lispy = self.is_written_in(view, languages)
188 | if settings.get('exclude_languages'):
189 | lispy = not lispy
190 | view.settings().set('rainbowth.lispy', lispy)
191 | if view.settings().get('rainbowth.lispy'):
192 | colors = self.update_color_scheme(view)
193 | view.settings().set('rainbowth.colors', colors)
194 | self.on_modified_async(view)
195 |
196 | def on_modified_async(self, view):
197 | if not view.settings().get('rainbowth.lispy'):
198 | return
199 | colors = view.settings().get('rainbowth.colors')
200 |
201 | settings = sublime.load_settings('Rainbowth.sublime-settings')
202 | customSigns = settings.get('custom_signs')
203 |
204 | if customSigns['enabled']:
205 | prefix = customSigns['prefix']
206 | suffix = customSigns['suffix']
207 | else:
208 | prefix = '(['
209 | suffix = ')]'
210 |
211 | disableString = settings.get('disable_inside_string')
212 | disableComment = settings.get('disable_inside_comment')
213 | regex = '['+re.escape(prefix) + re.escape(suffix)+']'
214 |
215 | badRegions = []
216 | if(disableComment):
217 | badRegions.extend(view.find_by_selector("comment"))
218 | if(disableString):
219 | badRegions.extend(view.find_by_selector("string"))
220 |
221 | level = -1
222 | per_line_depths = defaultdict(lambda: [[] for _ in range(len(colors))])
223 | for region in view.find_all(regex):
224 | skip = False
225 | for badRegion in badRegions:
226 | if badRegion.contains(region):
227 | skip = True
228 | if badRegion.contains(region):
229 | skip = True
230 |
231 | if not skip:
232 | char = view.substr(region)
233 | line, _ = view.rowcol(region.a)
234 | if char in prefix: level += 1
235 | per_line_depths[line][level % len(colors)].append(region)
236 | if char in suffix: level -= 1
237 |
238 | self.view_infos[view.id()] = ViewInfo(len(colors), per_line_depths)
239 | view.settings().set('rainbowth.line', None)
240 | self.on_selection_modified(view)
241 |
242 | def on_close(self, view):
243 | if not view.settings().get('rainbowth.lispy'):
244 | return
245 |
246 | del self.view_infos[view.id()]
247 |
248 | def on_selection_modified(self, view):
249 | if not view.settings().get('rainbowth.lispy'):
250 | return
251 | colors = view.settings().get('rainbowth.colors')
252 |
253 | if not view.id() in self.view_infos:
254 | return
255 |
256 | if len(view.sel()) == 1 and view.sel()[0].a == view.sel()[0].b:
257 | highlighted_line, _ = view.rowcol(view.sel()[0].a)
258 | else:
259 | highlighted_line = None
260 |
261 | old_highlighted_line = int(view.settings().get('rainbowth.line') or "-1")
262 | if old_highlighted_line == highlighted_line:
263 | return
264 | view.settings().set('rainbowth.line', highlighted_line)
265 |
266 | view_info = self.view_infos[view.id()]
267 | view_info.update(old_highlighted_line, highlighted_line)
268 | view_info.highlight(view)
269 |
--------------------------------------------------------------------------------