(core) Implement AI Assistant UI V2

Summary:
Implements the latest design of the Formula AI Assistant.

Also switches out brace to the latest build of ace.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D3949
This commit is contained in:
George Gevoian
2023-07-13 10:00:56 -04:00
parent 8581492912
commit ea8a59c5e9
23 changed files with 983 additions and 641 deletions

View File

@@ -1,3 +1,7 @@
.ace_editor {
background-color: var(--grist-theme-ace-editor-bg, white);
}
.ace_grist_link_hidden {
display: none;
}
@@ -14,6 +18,7 @@
.ace_editor.ace_autocomplete .ace_completion-highlight {
color: var(--grist-theme-ace-autocomplete-highlighted-fg, #000) !important;
text-shadow: 0 0 0.01em;
}
.ace_editor.ace_autocomplete .ace_completion-highlight.ace_grist_link {
@@ -41,3 +46,8 @@
.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
background-color: var(--grist-theme-ace-autocomplete-active-line-bg, #CAD6FA) !important;
}
.ace_autocomplete .ace_line .ace_ {
/* Ace collapses whitespace by default, which breaks alignment changes made in AceEditorCompletions.ts. */
white-space: pre !important;
}

View File

@@ -1,10 +1,11 @@
var ace = require('brace');
var ace = require('ace-builds');
var _ = require('underscore');
// Used to load python language settings and color themes
require('brace/mode/python');
require('brace/theme/chrome');
require('brace/theme/dracula');
require('brace/ext/language_tools');
// ace-builds also has a minified build (src-min-noconflict), but we don't
// use it since webpack already handles minification.
require('ace-builds/src-noconflict/mode-python');
require('ace-builds/src-noconflict/theme-chrome');
require('ace-builds/src-noconflict/theme-dracula');
require('ace-builds/src-noconflict/ext-language_tools');
var {setupAceEditorCompletions} = require('./AceEditorCompletions');
var {getGristConfig} = require('../../common/urlUtils');
var dom = require('../lib/dom');
@@ -291,7 +292,7 @@ AceEditor.prototype._setAceTheme = function(gristTheme) {
let _RangeConstructor = null; //singleton, load it lazily
AceEditor.makeRange = function(a, b, c, d) {
_RangeConstructor = _RangeConstructor || ace.acequire('ace/range').Range;
_RangeConstructor = _RangeConstructor || ace.require('ace/range').Range;
return new _RangeConstructor(a, b, c, d);
};

View File

@@ -1,14 +1,14 @@
import ace, {Ace} from 'ace-builds';
import {ISuggestionWithValue} from 'app/common/ActiveDocAPI';
import {commonUrls} from 'app/common/gristUrls';
import * as ace from 'brace';
export interface ICompletionOptions {
getSuggestions(prefix: string): Promise<ISuggestionWithValue[]>;
}
const completionOptions = new WeakMap<ace.Editor, ICompletionOptions>();
const completionOptions = new WeakMap<Ace.Editor, ICompletionOptions>();
export function setupAceEditorCompletions(editor: ace.Editor, options: ICompletionOptions) {
export function setupAceEditorCompletions(editor: Ace.Editor, options: ICompletionOptions) {
initCustomCompleter();
completionOptions.set(editor, options);
@@ -17,7 +17,7 @@ export function setupAceEditorCompletions(editor: ace.Editor, options: ICompleti
// It is important for autoSelect to be off so that hitting enter doesn't automatically
// use a suggestion, a change of behavior that doesn't seem particularly desirable and
// which also breaks several existing tests.
const {Autocomplete} = ace.acequire('ace/autocomplete'); // lives in brace/ext/language_tools
const {Autocomplete} = ace.require('ace/autocomplete');
const completer = new Autocomplete();
completer.autoSelect = false;
@@ -69,7 +69,7 @@ export function setupAceEditorCompletions(editor: ace.Editor, options: ICompleti
// it adds to body even when it detaches itself. Ace's AutoCompleter doesn't expose any
// interface for this, so this takes some hacking. (One reason for this is that Ace seems to
// expect that a single AutoCompleter would be used for all editor instances.)
editor.on('destroy', () => {
editor.on('destroy' as any, () => {
if (completer.editor) {
completer.detach();
}
@@ -91,10 +91,10 @@ function initCustomCompleter() {
const prefixMatchRegex = /\w+\.(?:lookupRecords|lookupOne)\([\w.$\u00A2-\uFFFF]*$|[\w.$\u00A2-\uFFFF]+$/;
// Monkey-patch getCompletionPrefix. This is based on the source code in
// node_modules/brace/ext/language_tools.js, simplified to do the one thing we want here (since
// the original method's generality doesn't help us here).
const util = ace.acequire('ace/autocomplete/util'); // lives in brace/ext/language_tools
util.getCompletionPrefix = function getCompletionPrefix(this: any, editor: ace.Editor) {
// node_modules/ace-builds/src-noconflict/ext-language_tools.js, simplified to do the one thing
// we want here (since the original method's generality doesn't help us here).
const util = ace.require('ace/autocomplete/util');
util.getCompletionPrefix = function getCompletionPrefix(this: any, editor: Ace.Editor) {
const pos = editor.getCursorPosition();
const line = editor.session.getLine(pos.row);
const match = line.slice(0, pos.column).match(prefixMatchRegex);
@@ -102,14 +102,14 @@ function initCustomCompleter() {
};
// Add some autocompletion with partial access to document
const aceLanguageTools = ace.acequire('ace/ext/language_tools');
const aceLanguageTools = ace.require('ace/ext/language_tools');
aceLanguageTools.setCompleters([]);
aceLanguageTools.addCompleter({
// For autocompletion we ship text to the sandbox and run standard completion there.
async getCompletions(
editor: ace.Editor,
session: ace.IEditSession,
pos: ace.Position,
editor: Ace.Editor,
session: Ace.EditSession,
pos: Ace.Position,
prefix: string,
callback: any
) {
@@ -120,12 +120,13 @@ function initCustomCompleter() {
// in the case where one function is being switched with another. Since we normally
// append a "(" when completing such suggestions, we need to be careful not to do
// so if a "(" is already present. One way to do this in ACE is to check if the
// current token is an identifier, and the next token is a lparen; if both are true,
// we skip appending a "(" to each suggestion.
// current token is a function/identifier, and the next token is a lparen; if both are
// true, we skip appending a "(" to each suggestion.
const wordRange = session.getWordRange(pos.row, pos.column);
const token = session.getTokenAt(pos.row, wordRange.end.column) as TokenInfo;
const nextToken = session.getTokenAt(pos.row, wordRange.end.column + 1) as TokenInfo|null;
const isRenamingFunc = token.type === 'identifier' && nextToken?.type === 'paren.lparen';
const token = session.getTokenAt(pos.row, wordRange.end.column) as Ace.Token;
const nextToken = session.getTokenAt(pos.row, wordRange.end.column + 1);
const isRenamingFunc = ['function.support', 'identifier'].includes(token.type)
&& nextToken?.type === 'paren.lparen';
const suggestions = await options.getSuggestions(prefix);
// ACE autocompletions are very poorly documented. This is somewhat helpful:
@@ -209,7 +210,8 @@ interface AceSuggestion {
* them to look like links, and handle clicks to open the destination URL.
*
* This implementation relies a lot on the details of the implementation in
* node_modules/brace/ext/language_tools.js. Updates to brace module may easily break it.
* node_modules/ace-builds/src-noconflict/ext-language_tools.js. Updates to ace-builds module may
* easily break it.
*/
function aceCompleterAddHelpLinks(completer: any) {
// Replace the $init function in order to intercept the creation of the autocomplete popup.
@@ -239,11 +241,7 @@ function customizeAceCompleterPopup(completer: any, popup: any) {
});
}
interface TokenInfo extends ace.TokenInfo {
type: string;
}
function retokenizeAceCompleterRow(rowData: AceSuggestion, tokens: TokenInfo[]): TokenInfo[] {
function retokenizeAceCompleterRow(rowData: AceSuggestion, tokens: Ace.Token[]): Ace.Token[] {
if (!(rowData.funcname || rowData.example)) {
// Not a special completion, pass through the result of ACE's original tokenizing.
return tokens;