(core) Formula autocomplete improvements for references and lookups

Summary:
Makes the following improvements to formula autocomplete:

- When a user types `$RefCol` (or part of it), also show `$RefCol.VisibleCol` (replace actual column names) in the autocomplete even before the `.` is typed, to help users understand the difference between a raw reference/record and its visible column.
- When a user types a table name, show `.lookupOne` and `.lookupRecords` in the autocomplete, again even before the `.` is typed.
- For `.lookupRecords(` and `.lookupOne(`, once the `(` is entered, suggest each column name as a keyword argument.
- Also suggest lookup arguments involving compatible reference columns, especially 'reverse reference' lookups like `refcol=$id` which are very common and difficult for users.
- To support these features, the Ace editor autocomplete needs some patching to fetch fresh autocomplete options after typing `.` or `(`. This also improves unrelated behaviour that wasn't great before when one column name is contained in another. See the first added browser test.

Discussions:

- https://grist.slack.com/archives/CDHABLZJT/p1659707068383179
- https://grist.quip.com/HoSmAlvFax0j#MbTADAH5kgG
- https://grist.quip.com/HoSmAlvFax0j/Formula-Improvements#temp:C:MbT3649fe964a184e8dada9bbebb

Test Plan: Added Python and nbrowser tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3580
This commit is contained in:
Alex Hall
2022-08-16 21:18:19 +02:00
parent 44b4ec7edf
commit 42060df29a
5 changed files with 325 additions and 43 deletions

View File

@@ -18,22 +18,6 @@ import usertypes
log = logger.Logger(__name__, logger.INFO)
def _make_sample_record(table_id, col_objs):
"""
Helper to create a sample record for a table, used for auto-completions.
"""
# This type gets created with a property for each column. We use property-methods rather than
# plain properties because this sample record is created before all tables have initialized, so
# reference values (using .sample_record for other tables) are not yet available.
RecType = type(table_id, (), {
# Note col=col to bind col at lambda-creation time; see
# https://stackoverflow.com/questions/10452770/python-lambdas-binding-to-local-values
col.col_id: property(lambda self, col=col: col.sample_value())
for col in col_objs
if column.is_user_column(col.col_id) or col.col_id == 'id'
})
return RecType()
def get_default_func_name(col_id):
return "_default_" + col_id
@@ -252,10 +236,33 @@ class Table(object):
"""
Used for auto-completion as a record with correct properties of correct types.
"""
return _make_sample_record(
self.table_id,
[col for col_id, col in self.all_columns.items() if col_id not in self._special_cols],
)
# Create a type with a property for each column. We use property-methods rather than
# plain attributes because this sample record is created before all tables have initialized, so
# reference values (using .sample_record for other tables) are not yet available.
props = {}
for col in self.all_columns.values():
if not (column.is_user_column(col.col_id) or col.col_id == 'id'):
continue
# Note c=col to bind at lambda-creation time; see
# https://stackoverflow.com/questions/10452770/python-lambdas-binding-to-local-values
props[col.col_id] = property(lambda _self, c=col: c.sample_value())
if col.col_id == 'id':
# The column lookup below doesn't work for the id column
continue
# For columns with a visible column (i.e. most Reference/ReferenceList columns),
# we also want to show how to get that visible column instead of the 'raw' record
# returned by the reference column itself.
col_rec = self._engine.docmodel.get_column_rec(self.table_id, col.col_id)
visible_col_id = col_rec.visibleCol.colId
if visible_col_id:
# This creates a fake attribute like `RefCol.VisibleCol` which isn't valid syntax normally,
# to show the `.VisibleCol` part before the user has typed the `.`
props[col.col_id + "." + visible_col_id] = property(
lambda _self, c=col, v=visible_col_id: getattr(c.sample_value(), v)
)
RecType = type(self.table_id, (), props)
return RecType()
def _rebuild_model(self, user_table):
"""