gaphor / gaphor

Gaphor is the simple modeling tool
https://gaphor.org
1.87k stars 200 forks source link

CSS autocompletion #1830

Closed vanillajonathan closed 1 year ago

vanillajonathan commented 2 years ago

Is your feature request related to a problem?

The user is not too familiar with CSS and which properties and functions exists.

Describe the solution you'd like

A GtkSource.CompletionProvider that provides autocompletion for CSS properties and functions.

Additional context

https://www.w3.org/Style/CSS/all-properties.en.html https://www.w3schools.com/cssref/index.php https://developer.mozilla.org/en-US/docs/Web/CSS/Reference

vanillajonathan commented 2 years ago

Something like this:

class CssPropertyCompletionProvider(GObject.GObject, GtkSource.CompletionProvider):
    PROPERTIES = [
        "all",
        "background",
        "background-color",
        "border",
        "border-bottom",
        "border-bottom-color",
        "border-bottom-style",
        "border-bottom-width",
        "border-collapse",
        "border-color",
        "border-radius",
        "color",
        "justify-content",
        "min-height",
        "min-width",
        "opacity",
        "padding",
        # Non-standard Gaphor-specific attributes
        "dash-style",
        "line-style",
        "line-width",
        "text-color",
    ]

    def __init__(self):
        super().__init__()
        self._filter_data: FilterData = FilterData()

    def do_activate(self, context: GtkSource.CompletionContext, proposal: GtkSource.CompletionProposal) -> None:
        buffer = context.get_buffer()
        buffer.begin_user_action()
        has_selection, begin, end = context.get_bounds()
        if has_selection:
            buffer.delete(begin, end)
        buffer.insert(begin, proposal.text, len(proposal.text))
        buffer.end_user_action()

    def do_display(self, context: GtkSource.CompletionContext, proposal: GtkSource.CompletionProposal, cell: GtkSource.CompletionCell) -> None:
        if cell.props.column == GtkSource.CompletionColumn.ICON:
            pass
        elif cell.props.column == GtkSource.CompletionColumn.TYPED_TEXT:
            cell.set_text(proposal.text)

    def do_get_priority(self, context: GtkSource.CompletionContext) -> int:
        return 1

    def do_get_title(self) -> str:
        return "CSS Properties"

    def do_is_trigger(self, textiter: Gtk.TextIter, ch :str) -> bool:
        buffer = textiter.get_buffer()
        if buffer.iter_has_context_class(textiter, "comment"):
            return False
        if ch.isalpha() or ch == "-":
            return True
        return False

    def do_key_activates(self, context, proposal, keyval: int, state) -> bool:
        return True

    def do_populate_async(self, context, cancellable, callback, user_data=None) -> None:
        task = Gio.Task.new(self, cancellable, callback)
        store = Gio.ListStore.new(CssPropertyProposal)
        self._filter_data.word = context.get_word()

        for prop in self.PROPERTIES:
            proposal = CssPropertyProposal(prop)
            store.append(proposal)

        filter_fn = lambda proposal, data: proposal.text.startswith(data.word)
        store_filter = Gtk.CustomFilter.new(filter_fn, self._filter_data)
        task.proposals = Gtk.FilterListModel.new(store, store_filter)
        task.return_boolean(True)

    def do_populate_finish(self, result: Gio.AsyncResult) -> Gio.ListModel:
        if result.propagate_boolean():
            return result.proposals

    def do_refilter(self, context: GtkSource.CompletionContext, model: Gio.ListModel) -> None:
        word = context.get_word()
        old_word = self._filter_data.word
        change = Gtk.FilterChange.DIFFERENT
        if old_word and word.startswith(old_word):
            change = Gtk.FilterChange.MORE_STRICT
        elif old_word and old_word.startswith(word):
            change = Gtk.FilterChange.LESS_STRICT
        self._filter_data.word = word
        model.get_filter().changed(change)

class CssPropertyProposal(GObject.Object, GtkSource.CompletionProposal):
    def __init__(self, text: str):
        super().__init__()
        self.text: str = text

class FilterData:
    word: str

Register the completion provider:

source_view = GtkSource.View()
provider = CssPropertyCompletionProvider()
view_completion = source_view.get_completion()
view_completion.add_provider(provider)
danyeaw commented 2 years ago

Hey @vanillajonathan, great idea! Would you be willing to help implement this?

vanillajonathan commented 2 years ago

Then I would have to setup a development environment.

The code above should be a good start, but the PROPERTIES property would need to be amended to contain all the standard CSS properties. The do_display method could be modified to also include a icon to symbolize a CSS property if such an icon were to be. The do_is_trigger method could be modified to inspect the Gtk.TextIter to conditionally trigger (such as not inside comments), the do_refilter could be implemented, but I don't know how to. An additional and separate class implementing CompletionProvider provider could be written for CSS functions. There could also be a CompletionProvider for named colors and at-rules.

The code above kind of works but does print some warning.

danyeaw commented 1 year ago

Then I would have to setup a development environment

You've got this! 😁

amolenaar commented 1 year ago

We're also using some non-standard CSS properties, so I suppose it would make sense to update the syntax highlighting as well.