Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion CSS/.python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.8
3.13
Copy link
Collaborator

Choose a reason for hiding this comment

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

ST 4201+ reads 3.8 as 3.13. Why not just leaving those files alone until py33 is removed?

Copy link
Collaborator

@FichteFoll FichteFoll Nov 2, 2025

Choose a reason for hiding this comment

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

Having them say 3.8 when the files actually require 3.13 is just wrong, though, even when ST 4201+ can handle it. This should also prevent issues when trying to run these Python files in an older ST build that does not have the 3.13 runtime.

I would be in favor of just removing them, if they are not required, but the 3.3 host is not dead yet and still the default, afaik.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't need typing in such simple plugins and beyond that the modules and this set of PRs doesn't add anything which judges breaking things for users which probably use cloned repos - at least not until public betas are available.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Typing annotations are basically free, assuming you stick to the various Python version requirements for various features. I would certainly not be against adding them if they don't break anything, e.g. if they were compatible with 3.8. If we're doing strict upgrades to 3.13 syntax (and without from __future__ import annotations), then I agree with you that breaking compatibility just for the heck of it isn't really worthwhile.

Either way, the .python-version should reflect the actually required Python version or be removed, imo.

79 changes: 40 additions & 39 deletions CSS/css_completions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
import sublime
import sublime_plugin

import re
import timeit

from functools import cached_property, wraps
Expand All @@ -17,63 +18,63 @@


def timing(func):
@wraps(func)
@wraps(wrapped=func)
def wrap(*args, **kw):
if ENABLE_TIMING:
ts = timeit.default_timer()
ts: float = timeit.default_timer()
result = func(*args, **kw)
if ENABLE_TIMING:
te = timeit.default_timer()
te: float = timeit.default_timer()
print(f"{func.__name__}({args}, {kw}) took: {1000.0 * (te - ts):2.3f} ms")
return result
return wrap


def match_selector(view, pt, scope):
def match_selector(view: sublime.View, pt: int, scope: str) -> bool:
# This will catch scenarios like:
# - .foo {font-style: |}
# - <style type="text/css">.foo { font-weight: b|</style>
return any(view.match_selector(p, scope) for p in (pt, pt - 1))
return any(view.match_selector(pt=p, selector=scope) for p in (pt, pt - 1))


def next_none_whitespace(view, pt):
def next_none_whitespace(view: sublime.View, pt: int) -> str | None:
for pt in range(pt, view.size()):
ch = view.substr(pt)
if ch not in ' \t':
return ch
return None


class CSSCompletions(sublime_plugin.EventListener):

@cached_property
def func_args(self):
def func_args(self) -> dict:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be nice to also have the dict generics here (and below).

return completions.get_func_args()

@cached_property
def props(self):
def props(self) -> dict:
return completions.get_properties()

@cached_property
def re_name(self):
def re_name(self) -> re.Pattern[str]:
return re.compile(r"([a-zA-Z-]+)\s*:[^:;{}]*$")

@cached_property
def re_value(self):
def re_value(self) -> re.Pattern[str]:
return re.compile(r"^(?:\s*(:)|([ \t]*))([^:]*)([;}])")

@timing
def on_query_completions(self, view, prefix, locations):

settings = sublime.load_settings('CSS.sublime-settings')
if settings.get('disable_default_completions'):
def on_query_completions(self, view: sublime.View, prefix: str, locations: list[int]) -> sublime.CompletionList | None:
settings: sublime.Settings = sublime.load_settings(base_name='CSS.sublime-settings')
if settings.get(key='disable_default_completions'):
return None

selector = settings.get('default_completions_selector', '')
selector = settings.get(key='default_completions_selector', default='')
if isinstance(selector, list):
selector = ''.join(selector)

pt = locations[0]
if not match_selector(view, pt, selector):
pt: int = locations[0]
if not match_selector(view, pt, scope=selector):
return None

if match_selector(view, pt, "meta.property-value.css meta.function-call.arguments"):
Expand All @@ -84,11 +85,11 @@ def on_query_completions(self, view, prefix, locations):
items = self.complete_property_name(view, prefix, pt)

if items:
return sublime.CompletionList(items)
return sublime.CompletionList(completions=items)
return None

def complete_property_name(self, view, prefix, pt):
text = view.substr(sublime.Region(pt, view.line(pt).end()))
def complete_property_name(self, view: sublime.View, prefix: str, pt: int) -> list[sublime.CompletionItem]:
text = view.substr(x=sublime.Region(a=pt, b=view.line(x=pt).end()))
matches = self.re_value.search(text)
if matches:
colon, space, value, term = matches.groups()
Expand All @@ -99,28 +100,28 @@ def complete_property_name(self, view, prefix, pt):
term = ""

# don't append anything if next character is a colon
suffix = ""
suffix: str = ""
if not colon:
# add space after colon if smart typing is enabled
if not space and view.settings().get("auto_complete_trailing_spaces"):
if not space and view.settings().get(key="auto_complete_trailing_spaces"):
suffix = ": $0"
else:
suffix = ":$0"

# terminate empty value if not within parentheses
if not value and not term and not match_selector(view, pt, "meta.group"):
if not value and not term and not match_selector(view, pt, scope="meta.group"):
suffix += ";"

return (
return [
sublime.CompletionItem(
trigger=prop,
completion=prop + suffix,
completion_format=sublime.COMPLETION_FORMAT_SNIPPET,
kind=KIND_CSS_PROPERTY
) for prop in self.props
)
]

def complete_property_value(self, view, prefix, pt):
def complete_property_value(self, view: sublime.View, prefix: str, pt: int) -> list[sublime.CompletionItem]:
completions = [
sublime.CompletionItem(
trigger="!important",
Expand All @@ -129,15 +130,15 @@ def complete_property_value(self, view, prefix, pt):
details="override any other declaration"
)
]
text = view.substr(sublime.Region(view.line(pt).begin(), pt - len(prefix)))
text: str = view.substr(x=sublime.Region(a=view.line(x=pt).begin(), b=pt - len(prefix)))
matches = self.re_name.search(text)
if matches:
prop = matches.group(1)
values = self.props.get(prop)
if values:
details = f"<code>{prop}</code> property-value"

if match_selector(view, pt, "meta.group") or next_none_whitespace(view, pt) == ";":
if match_selector(view, pt, scope="meta.group") or next_none_whitespace(view, pt) == ";":
suffix = ""
else:
suffix = "$0;"
Expand All @@ -161,25 +162,25 @@ def complete_property_value(self, view, prefix, pt):

return completions

def complete_function_argument(self, view: sublime.View, prefix, pt):
def complete_function_argument(self, view: sublime.View, prefix: str, pt: int) -> list[sublime.CompletionItem]:
func_name = ""
nest_level = 1
# Look for the beginning of the current function call's arguments list,
# while ignoring any nested function call or group.
for i in range(pt - 1, pt - 32 * 1024, -1):
ch = view.substr(i)
ch: str = view.substr(x=i)
# end of nested arguments list or group before caret
if ch == ")" and not view.match_selector(i, "string, comment"):
if ch == ")" and not view.match_selector(pt=i, selector="string, comment"):
nest_level += 1
continue
# begin of maybe nested arguments list or group before caret
if ch == "(" and not view.match_selector(i, "string, comment"):
if ch == "(" and not view.match_selector(pt=i, selector="string, comment"):
nest_level -= 1
# Stop, if nesting level drops below start value as this indicates the
# beginning of the arguments list the function name is of interest for.
if nest_level <= 0:
func_name = view.substr(view.expand_by_class(
i - 1, sublime.CLASS_WORD_START | sublime.CLASS_WORD_END))
func_name: str = view.substr(x=view.expand_by_class(
x=i - 1, classes=sublime.CLASS_WORD_START | sublime.CLASS_WORD_END))
break

if func_name == "var":
Expand All @@ -191,18 +192,18 @@ def complete_function_argument(self, view: sublime.View, prefix, pt):
details="var() argument"
)
for symbol in set(
view.substr(symbol_region)
for symbol_region in view.find_by_selector("entity.other.custom-property")
view.substr(x=symbol_region)
for symbol_region in view.find_by_selector(selector="entity.other.custom-property")
)
if not prefix or symbol.startswith(prefix)
]

args = self.func_args.get(func_name)
if not args:
return None
return []

completions = []
details = f"{func_name}() argument"
details: str = f"{func_name}() argument"
for arg in args:
if isinstance(arg, list):
completions.append(sublime.CompletionItem(
Expand Down