Skip to content

Support static member and constructor intelligence for R6, S4 and S7 - #784

Merged
renkun-ken merged 17 commits into
REditorSupport:masterfrom
renkun-ken:codex/r-polars-static-completion
Oct 8, 2026
Merged

renkun-ken merged 17 commits into
REditorSupport:masterfrom
renkun-ken:codex/r-polars-static-completion

Conversation

@renkun-ken

@renkun-ken renkun-ken commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Completion, signature help and hover currently lose receiver identity in chained $ calls. For example, q$group_by("Species")$ can show unrelated document tokens on its first trigger, and a method such as sum() is looked up by its bare name. This change infers the receiver from syntax, offers its members, and resolves method signatures and documentation through the inferred method identity. S4 slots and S7 properties accessed via @ use the same analysis and providers. S7 class bindings also supply constructor hover, signature help and argument completion. Member names can follow $/@ on later lines, and supported R6 method bodies resolve self, super and private against their declared class context.

Implementation

  • Infer S7 declarations, inheritance, unions, nested properties and supported custom constructors. Normalize current/legacy installed instances and class references, and extract installed formals. Derive source constructor semantics from installed S7 bodies/formals, including forwarding, overrides, direct constructor calls and list property updates. Support verified :=, S4 parents, external references with export/version checks and bounded dependency snapshots, S3 defaults and deprecation wrappers. Getter/setter values remain conservative.
  • Extract S4 class declarations and installed descriptors, including inherited slots, class generators, unions and dependency class identities. Infer nested slots and mixed @/$ chains, show slot types, and resolve function slot signatures when source syntax is known.
  • Add bounded abstract values and AST inference for source list/environment factories, lexical closures, fluent methods, declarative R6 inheritance, and supported package constructor/registry/S3-dispatch/delegation patterns.
  • Recover multiline member chains through whitespace/comments with replacement ranges on the member line. Recover supported R6 declarations within the existing bounds so method bodies see later members, inherited public/private methods and active properties. Respect local/formal shadowing, retain child receiver identity through parent methods, and isolate R cells in literate documents.
  • Prepare inert metadata from the user's installed packages in the package-resolution worker. No Polars API names or versioned method/return tables are built into production code. Package changes within supported patterns are reflected when metadata is rebuilt.
  • Share receiver inference across completion, signature help and hover, including quoted names, UTF-16 ranges, named-argument documentation, copied delegation formals and native bound-method formals.
  • Queue member requests until the current debounced parse is accepted; use existing cancellation/supersession handling. This fixes the first $ or @ trigger after an edit without adding a synchronous whole-document parse.

Document expressions, arguments, constructors, methods and active getters are never executed for these features. Package metadata preparation loads requested namespaces and can run package initialization. Unknown receivers/results remain conservative. The existing member_completion setting controls the shared member analysis.

Demos

The demo comment contains editable examples for:

  • Polars grouped queries, lazy frames and nested expression namespaces, including named-argument documentation.
  • Base-R list factories with nested return shapes and quoted fields.
  • Fluent environment methods returning self or a collected list.
  • R6 public inheritance and active member declarations without calling initializers/getters.
  • S4 source and installed-package examples are in the S4 demo comment.
  • S7 constructors, inherited/nested properties and function properties are in the S7 demo comment. All three blocks were verified through the static providers on S7 0.2.2. The S7 1.0 development update and demos cover the compatibility changes.
  • Multiline fluent pipelines and R6 self/super/private method-body references are in the multiline and R6 demo comment. Both examples were checked through the actual static providers.

All four examples were verified with the actual completion, signature and hover providers on R 4.6.1 with Polars 1.16.0, without executing their code. Experimental scripts, corpus output and standalone demos are excluded from the package; regression fixtures remain self-contained under tests/testthat.

Validation

  • The S7 suite passes 83 assertions on stable 0.2.2 and 116 on development revision 021846758e5d4520a56330d477066c25bd46d4f9 (0.2.2.9000, NEWS targets 1.0.0). Stable skips development-only features. Coverage-instrumented S7 tests pass on both versions; the development coverage run preceded the last two fixture assertions. CI now tests stable and that pinned development revision. This does not claim validation of a final unreleased 1.0 artifact.
  • S4/member-provider and language-feature handler suites pass. A long Polars q$ test reaches the existing 250 ms bound locally; the same failure reproduces on a fresh install of prior commit b40aa47. The latest full member suite is therefore not claimed to pass locally.
  • All 68 S4 assertions pass, including inheritance, generators, nested/dependency slots, unions, quoted names, Unicode, literate-region isolation, shadowing, bounded recursion, incomplete declarations, environment subclasses and immediate completion/signature/hover requests after edits.
  • The coverage-instrumented member/handler suites pass with 511 assertions and no failures, warnings or skips. The latest installed completion, completion-typing, signature, hover, member and language-feature handler suites also pass.
  • Multiline/R6 regressions cover later declarations, inherited private methods, active bodies, shadowing, unrelated trailing syntax errors, literate cells and immediate LSP requests after edits without retries. Changed-line lint and git diff --check pass.
  • Completion, signature, hover, member, completion-typing, inlay-hint, handler, text-sync, document, call-scanner and task-supersession regression suites pass using the installed branch.
  • Live LSP regressions with Polars 1.16.0 verify the first completion, signature and hover request after an edit, plus method argument documentation. The tested requests have no retry or sleep after didChange.
  • All four demo checks pass on R 4.6.1 with Polars 1.16.0.
  • R CMD build and R CMD check --no-tests --no-manual --no-build-vignettes pass with _R_CHECK_FORCE_SUGGESTS_=false. The full suite is not claimed to pass; formatting/diagnostics/code-action/protocol failures also reproduced on the pre-implementation branch in this environment. VS Code itself was not manually tested.

The analyzer is conservative and bounded: cursor recovery considers at most 129 lines / 64 KiB; inference has a 20,000-node, 64-level and 250 ms bound. Current limits include arbitrary cross-package/cross-file value flow, unsupported dynamic dispatch/NSE, schema-dependent names, dynamic R6 class mutations and declarations beyond the cursor recovery window. Unsupported patterns stay unknown; changed package architectures may require additional generic extraction rules.

An earlier production-engine corpus audit retained 133/168 article, 5,267/5,324 reference and 44/48 supplemental members. These are historical member-availability measurements at real $ nodes, not type precision or end-to-end editor coverage; research runners and captured corpus data are not part of this PR.

@renkun-ken

Copy link
Copy Markdown
Member Author

The demos now live in this comment; inst/demos and inst/experiments have been removed from the PR because neither is needed by the package or its regression tests.

Install the PR branch in the R library used by VS Code and restart the R language server. Keep r.lsp.member_completion enabled (the default). Paste each example into an R file; there is no need to run the example in the R session. For Polars, allow background package metadata preparation to finish after opening the file.

For completion, temporarily delete the member name after a chosen $ and trigger completion. For signature help, place the cursor inside the method's parentheses and use Trigger Parameter Hints. Hover over a method name or, for Polars, .maintain_order.

These examples were checked using the actual completion, signature and hover providers on R 4.6.1 with Polars 1.16.0, without sourcing or evaluating the demo files. This is automated provider verification, not a manual VS Code recording. Package metadata loading can run package initialization, but member analysis does not execute document expressions, arguments, constructors, methods or active getters.

Polars: query stages and expression namespaces

# Open this file with this branch's languageserver. Execution is optional.
# Completion: put the cursor after a $, deleting the following member name.
# Signature help: put the cursor inside a method's parentheses.
# Hover: point at a method name or named argument.

library(polars)

csv_file <- tempfile(fileext = ".csv")
write.csv(iris, csv_file, row.names = FALSE)

q <- pl$scan_csv(csv_file, infer_schema_files = 10)

# After the last $: agg, head, map_groups, ... (the grouped receiver).
# Hover group_by or .maintain_order for method/argument documentation.
grouped <- q$group_by("Species", .maintain_order = TRUE)
grouped$agg(pl$all()$sum())

# After the last $: collect, filter, group_by, ... (a lazy frame again).
result <- grouped$agg(pl$all()$sum())
result$collect()

# Nested namespace: complete/hover to_uppercase; signature: to_uppercase().
expression <- pl$col("Species")$str$to_uppercase()

# The Series receiver retains its return shape while delegating sum's docs.
series <- pl$Series("values", 1:3)
series$sum()

Base R: list factories, nested return shapes and quoted fields

# No extra packages. The provider analyzes the factory body and its closures.
make_client <- function(endpoint) {
    list(
        config = list(endpoint = endpoint, retries = 3L, `api key` = NULL),
        request = function(path, timeout = 30) {
            list(
                status = 200L,
                decode = function(simplify = TRUE) {
                    list(data = list(endpoint = endpoint, path = path), ok = TRUE)
                }
            )
        }
    )
}

client <- make_client("https://example.invalid")

# After client$: config, request.
# Signature help inside request(): request(path, timeout = 30).
response <- client$request("/records", timeout = 10)

# After response$: decode, status. Hover status shows the literal 200L.
# Signature help inside decode(): decode(simplify = TRUE).
decoded <- response$decode(simplify = FALSE)
response$status

# After decoded$: data, ok. After decoded$data$: endpoint, path.
decoded$data$path

# Quoted fields retain their full name and replacement/hover range.
client$config$`api key`

Base R: fluent environment methods

# No extra packages. Methods return self or a different statically known shape.
make_pipeline <- function(source) {
    self <- new.env(parent = emptyenv())
    self$source <- source
    self$filter <- function(predicate) self
    self$limit <- function(n = 10L) self
    self$collect <- function(format = "list") {
        list(data = source, format = format, metadata = list(cached = FALSE))
    }
    self
}

pipeline <- make_pipeline(iris)

# After the last $: collect, filter, limit, source.
# Signature help inside limit(): limit(n = 10L).
limited <- pipeline$filter("Sepal.Length > 5")$limit(n = 5L)

# Signature help inside collect(): collect(format = "list").
output <- limited$collect(format = "list")

# After output$: data, format, metadata. After metadata$: cached.
output$metadata$cached

R6: inherited methods and active member declarations

# R6 is a languageserver dependency. Public declarations supply the shape;
# the provider never calls initialize or the active getter to discover members.
Dataset <- R6::R6Class("Dataset", public = list(
    source = NULL,
    initialize = function(source) self$source <- source,
    select = function(columns) self,
    preview = function(n = 6L) list(data = self$source, rows = n)
))

LoggedDataset <- R6::R6Class("LoggedDataset",
    inherit = Dataset,
    public = list(log = function(message, level = "info") self),
    active = list(expensive = function(value) {
        stop("This getter must not run during language features")
    })
)

dataset <- LoggedDataset$new(iris)

# After the last $: inherited preview/select/source plus log/expensive.
# Signature help inside preview(): preview(n = 6L).
preview <- dataset$select(c("Species"))$log("selected")$preview(n = 3L)

# After preview$: data, rows. Hover rows shows its statically inferred value.
preview$rows

# Hover log/preview shows their respective signatures.
# expensive is offered as a declared member, but its result stays unknown.

The list example offers config and request after client$, then decode and status after response$. The environment example retains collect, filter, limit and source through fluent calls. The R6 example offers inherited preview, select and source alongside log and the declared expensive member; its getter result remains unknown. Method signatures and known field values are shown by hover. Unknown receivers/results stay unresolved.

@renkun-ken
renkun-ken requested review from eitsupi and randy3k October 7, 2026 07:53
@renkun-ken

renkun-ken commented Oct 7, 2026 •

Copy link
Copy Markdown
Member Author

S4 slot support is now included in this PR. These examples use the same static completion, hover and signature providers as the $ demos above. The providers do not execute the document, constructors, initialization/validity methods or slot values.

# Source declarations, inheritance, nested slots and mixed @/$ chains
Leaf <- methods::setClass("Leaf", slots = c(value = "numeric"))
Widget <- methods::setClass(
  "Widget",
  contains = "Leaf",
  slots = c(child = "Leaf", items = "list", run = "function"),
  prototype = list(run = function(value, flag = TRUE) value)
)

x <- new("Widget", child = Leaf(value = 1), items = list(alpha = 1))

x@                      # child, items, run, value; details show slot types
x@child@                # value
x@items$                # alpha
x@run(flag =            # run(value, flag = TRUE); active parameter: flag
x@value                 # hover: double

# Constructor aliases and source factories
factory <- function(value) Widget(items = list(result = value))
factory(unresolved)@items$  # result; no need to execute factory()

# Installed package metadata and a constructor's source return expression
library(stats4)
fit <- stats4::mle(function(mean = 0) sum((1:3 - mean)^2))
fit@                    # call, coef, details, fixed, fullcoef, method,
                        # min, minuslogl, nobs, vcov
fit@coef                # hover: double

The lines ending in @, $ or ( are editor completion/signature probes; select a member to finish them before sourcing the script. Function slot signatures are available when the function definition is visible in source or a constructor argument. A slot declared only as "function" does not provide enough information to infer its parameters. Installed class descriptors supply current slot types and inheritance; evaluated prototype values and initialization methods are deliberately not used to infer extra slot content.

Validation: all 68 S4 assertions pass, including a live LSP test that immediately requests completion, signature help and hover after didChange without a retry. The installed completion/member/handler suites pass, and the coverage-instrumented member/handler suites report 402 passing assertions with no failures, warnings or skips.

@renkun-ken renkun-ken changed the title Support static completion, signature help and hover for chained dollar members Support static completion, signature help and hover for dollar members and S4 slots Oct 7, 2026
@renkun-ken

Copy link
Copy Markdown
Member Author

Multiline chains and R6 method-body references are now covered in ba8626b. These examples can be pasted into an R document without running them. Completion, signature help and hover were checked through the actual providers; the live LSP tests also check the first request after an edit with retries disabled.

Multiline fluent environment methods

make_pipeline <- function(source) {
  self <- new.env(parent = emptyenv())
  self$source <- source
  self$filter <- function(predicate) self
  self$limit <- function(n = 10L) self
  self$collect <- function(format = "list") {
    list(data = source, format = format, metadata = list(cached = FALSE))
  }
  self
}
pipeline <- make_pipeline(iris)

pipeline$
  filter("Sepal.Length > 5")$
  limit(n = 5)$
  filter("Petal.Length < 2")$
  collect()

At each $, completion offers source, filter, limit and collect. On the following line, a typed member prefix filters the same candidates and replaces only the member token on that line. Hover and signature help resolve filter(predicate), limit(n = 10L) and collect(format = "list"). Blank lines and comments between the operator and member are also supported.

R6 self, super and private inside methods

Parent <- R6::R6Class("Parent", public = list(
  source = NULL,
  filter = function(predicate) self
))

Pipeline <- R6::R6Class("Pipeline", inherit = Parent,
  public = list(
    limit = function(n = 10L) {
      self$
        filter("Sepal.Length > 5")
      super$
        filter("Petal.Length < 2")
      private$remember(n)
      self
    },
    collect = function(format = "list") {
      list(data = self$source, format = format,
           metadata = list(cached = FALSE))
    }
  ),
  private = list(
    remember = function(n) self
  )
)

Pipeline$new()$
  limit(n = 5)$
  collect()$
  metadata$
  cached

Inside limit, completion for self$ includes source, filter, limit and the later collect declaration. Completion for super$ includes the inherited filter method; hover/signature help show filter(predicate). Hover/signature help on private$remember show remember(n). The final chain infers the collected list and its nested metadata$cached field.

R6 context is inferred from supported declarative R6Class definitions, including inheritance and public/private/active method bodies. Method parameters and local assignments can shadow self, super or private. Dynamic class mutations and declarations beyond the bounded recovery window remain outside the current inference support.

@renkun-ken

renkun-ken commented Oct 7, 2026 •

Copy link
Copy Markdown
Member Author

S7 constructors and @ properties now share the static analysis used by the member providers. These examples can be pasted into an R document without running them.

Class constructor and properties

library(S7)

Dog <- new_class("Dog", properties = list(
  name = class_character,
  age = class_numeric
))

Dog
lola <- Dog(name = "Lola", age = 11)
lola@age
  • Hover on Dog shows Dog(name = character(0), age = integer(0)).
  • Completion inside Dog( offers name = and age = ; signature help tracks the active argument.
  • Completion after lola@ offers age and name; hover on lola@age infers the supplied literal 11.
  • Dog()@age has declared type double | integer when its value is not known.
  • Dog@constructor( resolves the constructor member's formals; Dog() is the instance-construction call.

Inheritance and nested properties

NamedDog <- new_class("NamedDog", parent = Dog, properties = list(
  metadata = class_list
))

named <- NamedDog(name = "Lola", age = 11,
                 metadata = list(cached = FALSE))

named@
  metadata$
  cached

Completion after named@ offers inherited name and age plus metadata; the mixed multiline @/$ chain resolves cached. NamedDog( signature help includes inherited constructor arguments.

Custom constructors and function properties

Runner <- new_class("Runner",
  properties = list(run = class_function, items = class_list),
  constructor = function(first, option = TRUE) {
    new_object(S7_object(),
      run = function(value, flag = FALSE) value,
      items = list(done = first)
    )
  }
)

runner <- Runner(unresolved)
runner@run(flag = TRUE)
runner@items$done

Signature help and hover resolve Runner(first, option = TRUE) and run(value, flag = FALSE). Completion after runner@items$ offers done.

The analyzer reads installed class/property descriptors and constructor formals, and summarizes source declarations and supported custom-constructor syntax. It does not execute document expressions, constructors, getters, setters, validators or defaults. Getter/setter properties retain their declared types; values that depend on those callbacks remain unknown. Dynamic class definitions, arbitrary generic dispatch and cross-file value flow remain outside this support.

@renkun-ken renkun-ken changed the title Support static completion, signature help and hover for dollar members and S4 slots Support static member and constructor intelligence for R6, S4 and S7 Oct 7, 2026

@eitsupi eitsupi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed with ChatGPT against the latest b40aa47.

Overall, this has become a very impressive and carefully tested implementation. I did not find a general design or correctness issue with the R6/S4/member inference itself.

I do have one blocking concern before merging: S7 1.0 compatibility.

S7 main is already preparing 1.0.0 with breaking changes, and several of them directly affect assumptions made here:

  • R/member-metadata.R reads attr(value, "S7_class") directly. S7 1.0 has moved this internal attribute to _S7_class and explicitly recommends using S7_class() rather than accessing the attribute directly. The compatibility fallback is implemented inside S7, so it will not apply to this direct read.
  • S7 1.0 introduces the Foo := new_class() form. The current document binding analysis only tracks <- / = / ->, while the S7 inference requires an explicit string name, so this new form does not appear to be recognized.
  • The default constructor semantics for subclasses of a class with a custom constructor have changed in 1.0: the child now takes ... followed by its own properties and forwards ... to the parent. The static constructor synthesis here still appears to inherit the parent's formals directly, matching the 0.2.x behavior.

Since the current S7 regression tests exercise the released 0.2.2 behavior, I think we should test this feature against S7 main / the upcoming 1.0.0 before merging and adapt the introspection and source inference accordingly. Where possible, I would also prefer public S7 introspection APIs over direct reads of internal representation details.

One non-blocking architectural note from the ChatGPT review: this PR now establishes a substantial semantic-analysis subsystem shared by completion, hover, and signature help. I think that is coherent for this PR, but for future expansion it would be useful to keep a clear provider boundary so that alternative static-analysis engines, such as Arity, could eventually be integrated rather than duplicating increasingly broad semantic analysis inside languageserver.

Once the S7 1.0 compatibility issue is addressed, I would be comfortable with the overall direction of this PR.

@renkun-ken

renkun-ken commented Oct 7, 2026 •

Copy link
Copy Markdown
Member Author

Implemented the S7 1.0 development compatibility fixes identified in the audit. The source rules derive capabilities from the user's installed S7 function bodies/formals; installed classes supply their actual constructor formals. No versioned class/property API table is introduced, and inference still does not execute document expressions or callbacks.

This update handles current and legacy instance storage (including inert class-reference environments), nested exported instances, custom/foreign-parent forwarding constructors, overridden defaults, R matching at the dots boundary, direct @constructor() on supporting S7 versions, named/reordered object arguments and single-list property updates. It also adds verified S7 := declarations, | class unions, S4 parents, explicit S3 defaults, external class references with export/version checks and bounded worker dependency snapshots, and deprecation declarations. Named argument completion no longer offers ... = .

The new compatibility workflow tests S7 0.2.2 and development commit 021846758e5d4520a56330d477066c25bd46d4f9. This revision reports 0.2.2.9000 and its NEWS targets 1.0.0; this is not a claim of testing a final 1.0 release.

Try these examples with that S7 development revision. At @ completion lists the properties; hovering a class or requesting signature help shows the corresponding constructor.

library(S7)

Dog := new_class(properties = list(name = class_character, age = class_numeric))
lola <- Dog(name = "Lola", age = 11)
lola@age
Dog@constructor()@age

Parent := new_class(
  properties = list(payload = class_list),
  constructor = function(seed = 1L) {
    new_object(S7_object(), payload = list(seed = seed))
  }
)
Child := new_class(parent = Parent, properties = list(label = class_character))
# Signature: Child(..., label = character(0))
Child(3L, label = "child")@payload$seed

Box := new_class(properties = list(
  items = new_property(class_list, default = quote(list(old = 1)))
))
# Completion after the last $ suggests new, rather than old.
set_props(Box(), list(items = list(new = 2)))@items$new

S4Base <- methods::setClass("S4Base", slots = c(value = "numeric"))
Hybrid := new_class(parent = S4Base, properties = list(label = class_character))
Hybrid(value = 1, label = "hybrid")@value

Validation: the focused S7 suite passes on stable 0.2.2 (83 assertions; new-version-only features are skipped) and the pinned development version (116 assertions). Stable S7 coverage tests pass; the development coverage run passed 112 assertions before the last four fixture assertions were added. S4/member-provider and language-feature handler checks pass. Changed-line lint and git diff --check pass. The package builds and the clean no-tests R CMD check passes.

The full member suite reaches an existing 250 ms limit in the long Polars q$ case on this macOS environment. That failure reproduces on a fresh install of the previous PR commit b40aa47; it is not counted as a passing broader suite. With coverage's existing timing allowance, the broader development member run passed 415 assertions before a test-fixture declaration error; that fixture has since been corrected and the focused suite rerun. Generic S7 method dispatch/conversions and arbitrary environment mutation remain outside the supported static summaries. Unsupported external dependencies remain unknown.

Final review also prevents setter properties from retaining stale declared-default member shapes. Both S7 CI matrix jobs (stable 0.2.2 and pinned development 0.2.2.9000) and lint passed on final commit 25b16ef. Existing R CMD check and coverage shard jobs are still running.

@eitsupi eitsupi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed with ChatGPT against the latest 25b16ef.

The blocking S7 1.0 concerns from my previous review have been addressed comprehensively, and the compatibility CI now covers both S7 0.2.2 and the current S7 main revision. I also re-reviewed the abstraction boundaries in the member analysis more closely.

Overall, I think the architecture is sound for this PR. Completion, hover, and signature help correctly share the same receiver inference and abstract-value machinery, and the R6, S4, and S7-specific semantics are mostly kept in separate modules. I do not think further architectural refactoring should block this PR.

I did notice a few non-blocking abstraction issues that would be worth keeping in mind as this subsystem grows:

  • member_slot() is effectively the generic @ dispatcher, but it currently lives in member-s7.R and falls back to member_s4_slot(). The dependency direction would be clearer if the common layer dispatched to separate S4/S7 implementations, e.g. member_s4_slot() and member_s7_slot().
  • member_value() has explicit S4/R6 state, while S7-specific fields such as s7_descriptor and s7_generator are added later by member_s7_value(). Common code such as member_shape_key() already knows about those S7 fields, so the abstract-value schema is slightly asymmetric. If this grows further, it may be worth formalizing the object-system-specific payload.
  • The provider layer currently identifies constructor intelligence through s7_generator. That is sufficient today, but if constructor support later expands to S4 generators, R6 factories, or other class systems, a generic constructor/callable capability would avoid accumulating object-system checks in the providers.
  • member_context_index() also knows how to merge S4/S7 dependency metadata and attached-object-system state. This is still manageable, but package/object-system-specific index merging may deserve its own boundary if more semantic systems are added.

These are architectural follow-ups rather than correctness concerns. In particular, I would prefer not to over-generalize the internals just to anticipate another backend. A cleaner long-term direction would be to keep this native member engine cohesive, then place a provider boundary around it if languageserver later supports alternative static-analysis backends such as Arity.

With the previous S7 compatibility concerns resolved, LGTM. Approving.

@renkun-ken

Copy link
Copy Markdown
Member Author

@eitsupi Thanks for the review. Will take a closer look at the abstraction issues you mentioned in a follow-up work.

@renkun-ken
renkun-ken merged commit 7b613b5 into REditorSupport:master Oct 8, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants