Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Color Picker support in LSP #2053

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
07223ea
make color box clickable
predragnikolic Sep 7, 2022
d34c06c
Finalize color picker implementation
predragnikolic Sep 7, 2022
b5e292b
fix box rendering and import the command from the right place
predragnikolic Sep 7, 2022
1c02247
smaller color boxes
predragnikolic Sep 7, 2022
e3de718
add back margin top to make the color box look centered
predragnikolic Sep 7, 2022
22c0aab
- bundle color pickers
predragnikolic Sep 8, 2022
f26d2d9
fix stylelint
predragnikolic Sep 8, 2022
113f339
add abstraction for creating Color Pickers
predragnikolic Sep 8, 2022
23957e9
add windows executable,
predragnikolic Sep 8, 2022
c464502
add windows color picker executable
predragnikolic Sep 8, 2022
4fc9bcd
add a normalize_color to enusre that each ColorPicker implementatoin …
predragnikolic Sep 8, 2022
ea17403
fix lint
predragnikolic Sep 8, 2022
b0da79c
float map, also add fallback
predragnikolic Sep 8, 2022
fa50534
no need to fallback
predragnikolic Sep 8, 2022
df001a4
remove ColorPickResult
predragnikolic Sep 8, 2022
751858b
move everthing to one file
predragnikolic Sep 9, 2022
ef59300
add a windows_executable
predragnikolic Sep 9, 2022
4c60249
asdfjklc
predragnikolic Sep 9, 2022
09f8008
make to color picker window on display on top
predragnikolic Sep 9, 2022
81fcfbf
this is the best I can do on windows for now
predragnikolic Sep 9, 2022
03d3ea0
add mac picker
predrag-codetribe Sep 12, 2022
0d02c85
reduce margin top
predragnikolic Sep 13, 2022
4754306
make sure to close the previous color picker before trying to pick a …
predragnikolic Sep 13, 2022
4ac908e
add comment
predragnikolic Sep 13, 2022
12fbff0
empty string is also a string, add check if string is truthy to avoid…
predragnikolic Sep 13, 2022
9f1a540
ignore errors if the process was already killed
predragnikolic Sep 13, 2022
1661538
Merge branch 'main' into feat/add-color-picker-support-v2-bundle-them…
predrag-codetribe Sep 14, 2022
ab8352b
Update plugin/color_picker/__init__.py
predragnikolic Sep 16, 2022
51eb05f
add sys import
predrag-codetribe Sep 16, 2022
ebb9f4d
remove test file
predrag-codetribe Sep 16, 2022
b1ac99b
add back the if check
predrag-codetribe Sep 16, 2022
7dd6a3b
add platform checks
predrag-codetribe Sep 16, 2022
8a1e5cd
hack - make the hole box clickable
predrag-codetribe Sep 16, 2022
ad43def
an image and display block will make the whole color box clickable
predragnikolic Sep 19, 2022
f06895c
use  
predrag-codetribe Sep 20, 2022
a6d1e47
remove win_colorpicker.exe
predrag-codetribe Sep 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from .plugin.core.panels import LspUpdateLogPanelCommand
from .plugin.core.panels import WindowPanelListener
from .plugin.core.protocol import Error
from .plugin.color_picker import CloseColorPickerOnBlur
from .plugin.core.protocol import Location
from .plugin.core.protocol import LocationLink
from .plugin.core.registry import LspRecheckSessionsCommand
Expand All @@ -39,6 +40,7 @@
from .plugin.core.transports import kill_all_subprocesses
from .plugin.core.typing import Any, Optional, List, Type, Dict, Union
from .plugin.core.views import get_uri_and_position_from_location
from .plugin.color_picker import LspChooseColorPicker
from .plugin.core.views import LspRunTextCommandHelperCommand
from .plugin.document_link import LspOpenLinkCommand
from .plugin.documents import DocumentSyncListener
Expand Down
242 changes: 242 additions & 0 deletions plugin/color_picker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
from ..core.protocol import Color, TextEdit, ColorInformation
from ..core.typing import Callable, Optional, List, Any
from ..core.views import lsp_color_to_hex
from ..formatting import apply_text_edits_to_view
from abc import ABCMeta
import os
import sublime
import sublime_plugin
import subprocess
import sys
import threading

OnPickCallback = Callable[[Optional[Color]], None]


class ColorPickerPlugin(metaclass=ABCMeta):
def pick(self, on_pick: OnPickCallback, preselect_color: Optional[Color] = None) -> None:
...

def normalize_color(self, color: Any) -> Optional[Color]:
...

def close(self) -> None:
...


if sublime.platform() == 'linux':
class LinuxColorPicker(ColorPickerPlugin):
process = None # type: Optional[subprocess.Popen]

def pick(self, on_pick: OnPickCallback, preselect_color: Optional[Color] = None) -> None:
self.close()
t = threading.Thread(target=self._open_picker, args=(on_pick, preselect_color))
t.start()

def _open_picker(self, on_pick: OnPickCallback, color: Optional[Color] = None) -> None:
preselect_color_arg = ""
if color:
preselect_color_arg = "{},{},{},{}".format(
color['red'], color['green'], color['blue'], color['alpha']
)
picker_cmd = [
os.path.join(sublime.packages_path(), "LSP", "plugin", "color_picker", "linux_executable.py"),
preselect_color_arg
]
self.process = subprocess.Popen(picker_cmd, stdout=subprocess.PIPE)
output = self.process.communicate()[0].strip().decode('utf-8')
on_pick(self.normalize_color(output))

def normalize_color(self, color: Any) -> Optional[Color]:
if color and isinstance(color, str):
r, g, b, a = map(float, color.split(','))
return {
"red": r,
"green": g,
"blue": b,
"alpha": a,
}
return None

def close(self) -> None:
if self.process:
try:
self.process.kill()
except:
pass
self.process = None


if sys.platform == "win32":
import ctypes

class CHOOSECOLOR(ctypes.Structure):
_fields_ = [
("lStructSize", ctypes.c_uint32),
("hwndOwner", ctypes.c_void_p),
("hInstance", ctypes.c_void_p),
("rgbResult", ctypes.c_uint32),
("lpCustColors", ctypes.POINTER(ctypes.c_uint32)),
("Flags", ctypes.c_uint32),
("lCustData", ctypes.c_void_p),
("lpfnHook", ctypes.c_void_p),
("lpTemplateName", ctypes.c_wchar_p)]

CC_SOLIDCOLOR = 0x80
CC_RGBINIT = 0x01
CC_FULLOPEN = 0x02
ChooseColorW = ctypes.windll.Comdlg32.ChooseColorW
ChooseColorW.argtypes = [ctypes.POINTER(CHOOSECOLOR)]
ChooseColorW.restype = ctypes.c_int32


class WindowsColorPicker(ColorPickerPlugin):
def pick(self, on_pick: OnPickCallback, preselect_color: Optional[Color] = None) -> None:
self.close()
default_color = (255 << 16) | (255 << 8) | (255)
if preselect_color:
default_color = (round(255*preselect_color['blue']) << 16) | (round(255*preselect_color['green']) << 8) | round(255*preselect_color['red'])
cc = CHOOSECOLOR()
ctypes.memset(ctypes.byref(cc), 0, ctypes.sizeof(cc))
cc.lStructSize = ctypes.sizeof(cc)
cc.hwndOwner = sublime.active_window().hwnd()
CustomColors = ctypes.c_uint32 * 16
cc.lpCustColors = CustomColors() # uses 0 (black) for all 16 predefined custom colors
cc.rgbResult = ctypes.c_uint32(default_color)
cc.Flags = CC_SOLIDCOLOR | CC_FULLOPEN | CC_RGBINIT

# ST window will become unresponsive until color picker dialog is closed
output = ChooseColorW(ctypes.byref(cc))

if output == 1: # user clicked OK
on_pick(self.normalize_color(cc.rgbResult))
else:
on_pick(None)

def normalize_color(self, bgr_color: Any) -> Optional[Color]:

def bgr2color(bgr) -> Color:
# 0x00BBGGRR
byte_table = list(["{0:02X}".format(b) for b in range(256)])
b_hex = byte_table[(bgr >> 16) & 0xff]
g_hex = byte_table[(bgr >> 8) & 0xff]
r_hex = byte_table[(bgr) & 0xff]

r = int(r_hex, 16) / 255
g = int(g_hex, 16) / 255
b = int(b_hex, 16) / 255
return {
"red": r,
"green": g,
"blue": b,
"alpha": 1 # Windows color picker doesn't support alpha, so fallback to 1
}

if bgr_color:
return bgr2color(bgr_color)
return None

def close(self) -> None:
pass # on windows, the color picker will block until a color is choosen


if sublime.platform() == 'osx':
class OsxColorPicker(ColorPickerPlugin):
process = None # type: Optional[subprocess.Popen]
script = '''
function run(argv) {{
var app = Application.currentApplication();
app.includeStandardAdditions = true;
var color = app.chooseColor({{defaultColor: [{red}, {green}, {blue}]}});
console.log(JSON.stringify(color));
}}
'''

def pick(self, on_pick: OnPickCallback, preselect_color: Optional[Color] = None) -> None:
self.close()
t = threading.Thread(target=self._open_picker, args=(on_pick, preselect_color))
t.start()

def _open_picker(self, on_pick: OnPickCallback, color: Optional[Color] = None) -> None:
script = self.script.format(red=0, green=0, blue=0)
if color:
script = self.script.format(red=color['red'], green=color['green'], blue=color['blue'])
picker_cmd = ["/usr/bin/osascript", "-l", "JavaScript", "-e", script]
self.process = subprocess.Popen(picker_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = self.process.communicate()[0].strip().decode('ascii')
on_pick(self.normalize_color(output))

def normalize_color(self, color: Any) -> Optional[Color]:
print('OSX output contains a warning:', color)
if color and isinstance(color, list):
r, g, b = 1, 1, 1 # hardcoded
return {
"red": r,
"green": g,
"blue": b,
"alpha": 1, # OSX color picker doesn't support alpha, so fallback to 1
}
return None

def close(self) -> None:
if self.process:
try:
self.process.kill()
except:
pass
self.process = None


def get_color_picker() -> Optional[ColorPickerPlugin]:
if sublime.platform() == "linux":
return LinuxColorPicker()
if sublime.platform() == "windows":
return WindowsColorPicker()
if sublime.platform() == "osx":
return OsxColorPicker()
return None


color_picker = get_color_picker()


class CloseColorPickerOnBlur(sublime_plugin.EventListener):
def on_activated(self, view: sublime.View) -> None:
if color_picker:
color_picker.close()

def on_exit(self) -> None:
if color_picker:
color_picker.close()


class LspChooseColorPicker(sublime_plugin.TextCommand):
def run(self, edit: sublime.Edit, color_information: ColorInformation, file_name: str) -> None:
if not color_picker:
sublime.status_message('Your platform does not support a ColorPicker yet.')
return
window = self.view.window()
if not window:
return

def on_select(color: Optional[Color]) -> None:
self.on_pick_color(color, color_information, file_name)

color_picker.pick(on_select, color_information['color'])

def on_pick_color(
self, selected_color: Optional[Color], color_information: ColorInformation, file_name: str
) -> None:
if not selected_color:
return
window = self.view.window()
if not window:
return
view = window.find_open_file(file_name)
new_text = lsp_color_to_hex(selected_color)
text_edits = [{
"newText": new_text,
"range": color_information['range']
}] # type: List[TextEdit]
if view:
apply_text_edits_to_view(text_edits, view)
23 changes: 23 additions & 0 deletions plugin/color_picker/linux_executable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env python
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is shebang necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check when I'm on linux.

import gi
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this package is not installed by default?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is installed by default in GNOME and other GTK distrons,
but I guess it is not installed on KDE or other distros that use Qt.

That said, currently this script doesn't support all linux versions.
We could do something like ColorHelper https://github.com/facelessuser/ColorHelper/releases/tag/st3-3.1.0
.

Or something like ColorPicker, to have a fallback in the script
https://github.com/weslly/ColorPicker/blob/master/lib/linux_colorpicker.py#L21

import sys
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk

color_chooser_dialog = Gtk.ColorChooserDialog(show_editor=True)
color_chooser_dialog.set_title('LSP Color Picker')

if len(sys.argv) > 1: # sys.argv[1] looks like '1,0.2,1,0.5'
r, g, b, a = map(float, sys.argv[1].split(','))
preselect_color = Gdk.RGBA(r, g, b, a)
color_chooser_dialog.set_rgba(preselect_color)

def on_select_color():
color = color_chooser_dialog.get_rgba()
print('{},{},{},{}'.format(color.red, color.green, color.blue, color.alpha))

if color_chooser_dialog.run() == Gtk.ResponseType.OK:
on_select_color()

color_chooser_dialog.destroy()
12 changes: 12 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ class SemanticTokenModifiers:
'end': Position
})

Color = TypedDict('Color', {
'red': float,
'green': float,
'blue': float,
'alpha': float
}, total=True)

ColorInformation = TypedDict('ColorInformation', {
'range': RangeLsp,
'color': Color
}, total=True)

TextDocumentIdentifier = TypedDict('TextDocumentIdentifier', {
'uri': DocumentUri,
}, total=True)
Expand Down
52 changes: 35 additions & 17 deletions plugin/core/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from .css import css as lsp_css
from .protocol import CodeAction
from .protocol import CodeActionContext
from .protocol import CodeActionParams
from .protocol import CodeActionTriggerKind
from .protocol import CodeAction, Color, ColorInformation
from .protocol import Command
from .protocol import CompletionItem
from .protocol import CompletionItemKind
Expand Down Expand Up @@ -763,28 +763,46 @@ def run(self, view_id: int, command: str, args: Optional[Dict[str, Any]] = None)


COLOR_BOX_HTML = """
<style>html {{padding: 0; background-color: transparent}}</style>
<style>
html {{padding: 0; background-color: transparent}}

#lsp-color-box a {{
border: 1px solid color(var(--foreground) alpha(0.25));
background-color: {backgroundColor};
margin-top: 0.1em;
height: 1rem;
width: 1rem;
display: block;
}}
</style>
<body id='lsp-color-box'>
<div style='padding: 0.4em;
margin-top: 0.2em;
border: 1px solid color(var(--foreground) alpha(0.25));
background-color: rgba({}, {}, {}, {})'>
</div>
<a href="{command}">&nbsp;</a>
</body>"""


def lsp_color_to_html(color_info: Dict[str, Any]) -> str:
color = color_info['color']
red = color['red'] * 255
green = color['green'] * 255
blue = color['blue'] * 255
alpha = color['alpha']
return COLOR_BOX_HTML.format(red, green, blue, alpha)
def lsp_color_to_hex(color: Color) -> str:
red = int(color['red'] * 255)
green = int(color['green'] * 255)
blue = int(color['blue'] * 255)
alpha = int(color['alpha'] * 255)

if color['alpha'] < 1:
return "#{:02x}{:02x}{:02x}{:02x}".format(red, green, blue, alpha)
return "#{:02x}{:02x}{:02x}".format(red, green, blue)

def lsp_color_to_phantom(view: sublime.View, color_info: Dict[str, Any]) -> sublime.Phantom:
region = range_to_region(Range.from_lsp(color_info['range']), view)
return sublime.Phantom(region, lsp_color_to_html(color_info), sublime.LAYOUT_INLINE)

def lsp_color_to_html(view: sublime.View, color_information: ColorInformation) -> str:
color = lsp_color_to_hex(color_information['color'])
command = sublime.command_url('lsp_choose_color_picker', {
'color_information': color_information,
'file_name': view.file_name()
})
return COLOR_BOX_HTML.format(command=command, backgroundColor=color)


def lsp_color_to_phantom(view: sublime.View, color_information: ColorInformation) -> sublime.Phantom:
region = range_to_region(Range.from_lsp(color_information['range']), view)
return sublime.Phantom(region, lsp_color_to_html(view, color_information), sublime.LAYOUT_INLINE)


def document_color_params(view: sublime.View) -> Dict[str, Any]:
Expand Down
6 changes: 3 additions & 3 deletions plugin/session_buffer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .core.protocol import ColorInformation
from .core.protocol import Diagnostic
from .core.protocol import DiagnosticSeverity
from .core.protocol import DocumentLink
Expand Down Expand Up @@ -351,10 +352,9 @@ def _do_color_boxes_async(self, view: sublime.View, version: int) -> None:
Request.documentColor(document_color_params(view), view),
self._if_view_unchanged(self._on_color_boxes_async, version)
)

def _on_color_boxes_async(self, view: sublime.View, response: Any) -> None:
def _on_color_boxes_async(self, view: sublime.View, response: List[ColorInformation]) -> None:
color_infos = response if response else []
self.color_phantoms.update([lsp_color_to_phantom(view, color_info) for color_info in color_infos])
self.color_phantoms.update([lsp_color_to_phantom(view, color_information) for color_information in color_infos])

# --- textDocument/documentLink ------------------------------------------------------------------------------------

Expand Down