diff --git a/app/gui2/src/assets/icons.svg b/app/gui2/src/assets/icons.svg index 10fb1a93da..cacf34176d 100644 --- a/app/gui2/src/assets/icons.svg +++ b/app/gui2/src/assets/icons.svg @@ -1,911 +1,1602 @@ - + - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + - - - - - - + + - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + - - - - - - - - - + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + - + + + + + + - + - - - - - - + + + + - - - - - - - - - - - - - - - - + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + +
- + diff --git a/app/gui2/src/components/ComponentBrowser/__tests__/filtering.test.ts b/app/gui2/src/components/ComponentBrowser/__tests__/filtering.test.ts index 4153d30a53..00eea61c92 100644 --- a/app/gui2/src/components/ComponentBrowser/__tests__/filtering.test.ts +++ b/app/gui2/src/components/ComponentBrowser/__tests__/filtering.test.ts @@ -20,15 +20,21 @@ test.each([ { ...makeStaticMethod('Standard.Base.Data.Vector.Vector.new'), groupIndex: 1 }, makeModule('local.New_Project'), makeModule('Standard.Base.Data'), + makeModuleMethod('Standard.Base.Data.read_text'), + makeStaticMethod('local.Project.Foo.new'), + makeStaticMethod('local.Project.Internalization.internalize'), ])('$name entry is in the CB main view', (entry) => { const filtering = new Filtering({}) expect(filtering.filter(entry)).not.toBeNull() }) test.each([ - makeModuleMethod('Standard.Base.Data.convert'), // not in group + makeModuleMethod('Standard.Base.Data.Vector.some_method'), // not in top group { ...makeMethod('Standard.Base.Data.Vector.Vector.get'), groupIndex: 1 }, // not static method makeModule('Standard.Base.Data.Vector'), // Not top module + makeStaticMethod('Standard.Base.Internal.Foo.bar'), // Internal method + makeModule('Standard.Base.Internal'), // Internal module + makeModule('Standard.Internal.Foo'), // Internal project ])('$name entry is not in the CB main view', (entry) => { const filtering = new Filtering({}) expect(filtering.filter(entry)).toBeNull() @@ -63,6 +69,18 @@ test.each([ expect(substringFiltering.filter(entry)).toBeNull() }) +test('Internal entry is not in the Standard.Base.Data content', () => { + const entry = makeModule('Standard.Base.Data.Internal') + const filtering = new Filtering({ qualifiedNamePattern: 'Standard.Base.Data' }) + expect(filtering.filter(entry)).toBeNull() +}) + +test('The content of an internal module is displayed', () => { + const entry = makeModuleMethod('Standard.Base.Data.Internal.foo') + const filtering = new Filtering({ qualifiedNamePattern: 'Standard.Base.Data.Internal' }) + expect(filtering.filter(entry)).not.toBeNull() +}) + test.each([ makeModuleMethod('local.Project.Module.foo'), makeModuleMethod('local.Project.Module.Submodule.foo_in_submodule'), diff --git a/app/gui2/src/components/ComponentBrowser/filtering.ts b/app/gui2/src/components/ComponentBrowser/filtering.ts index 0b1f022a8d..2b6e2dec9b 100644 --- a/app/gui2/src/components/ComponentBrowser/filtering.ts +++ b/app/gui2/src/components/ComponentBrowser/filtering.ts @@ -285,6 +285,7 @@ export class Filtering { showUnstable: boolean = false showLocal: boolean = false currentModule?: QualifiedName + browsingInternalModule: boolean = false constructor(filter: Filter, currentModule: Opt = undefined) { const { pattern, selfArg, qualifiedNamePattern, showUnstable, showLocal } = filter @@ -295,6 +296,7 @@ export class Filtering { if (qualifiedNamePattern) { this.qualifiedName = new FilteringQualifiedName(qualifiedNamePattern) this.fullPattern = pattern ? `${qualifiedNamePattern}.${pattern}` : qualifiedNamePattern + this.browsingInternalModule = isInternalModulePath(qualifiedNamePattern) } else if (pattern) this.fullPattern = pattern if (this.fullPattern) { let prefix = '' @@ -334,9 +336,11 @@ export class Filtering { private mainViewFilter(entry: SuggestionEntry): MatchResult | null { const hasGroup = entry.groupIndex != null const isModule = entry.kind === SuggestionKind.Module - const isTopElement = qnIsTopElement(entry.definedIn) - if (!hasGroup && (!isModule || !isTopElement)) return null - else return { score: 0 } + const isMethod = entry.kind === SuggestionKind.Method + const isInTopModule = qnIsTopElement(entry.definedIn) + const isTopElementInMainView = (isMethod || isModule) && isInTopModule + if (hasGroup || isTopElementInMainView) return { score: 0 } + else return null } private isLocal(entry: SuggestionEntry): boolean { @@ -344,18 +348,28 @@ export class Filtering { } filter(entry: SuggestionEntry): MatchResult | null { - let qualifiedNameMatch: Opt if (entry.isPrivate) return null - else if (!this.selfTypeMatches(entry)) return null - else if (!(qualifiedNameMatch = this.qualifiedNameMatches(entry))) return null - else if (!this.showUnstable && entry.isUnstable) return null - else if (this.showLocal && !this.isLocal(entry)) return null - else if (this.pattern) { + if (this.selfArg == null && isInternal(entry) && !this.browsingInternalModule) return null + if (!this.selfTypeMatches(entry)) return null + const qualifiedNameMatch = this.qualifiedNameMatches(entry) + if (!qualifiedNameMatch) return null + if (!this.showUnstable && entry.isUnstable) return null + if (this.showLocal && !this.isLocal(entry)) return null + if (this.pattern) { const patternMatch = this.pattern.tryMatch(entry) if (!patternMatch) return null if (!this.showLocal && this.isLocal(entry)) patternMatch.score *= 2 return { ...qualifiedNameMatch, ...patternMatch } - } else if (this.isMainView()) return this.mainViewFilter(entry) - else return { score: 0 } + } + if (this.isMainView()) return this.mainViewFilter(entry) + return { score: 0 } } } + +function isInternal(entry: SuggestionEntry): boolean { + return isInternalModulePath(entry.definedIn) +} + +function isInternalModulePath(path: string): boolean { + return /Standard[.].*Internal(?:[._]|$)/.test(path) +} diff --git a/app/gui2/src/stores/project.ts b/app/gui2/src/stores/project.ts index 27811ad8c1..02489da718 100644 --- a/app/gui2/src/stores/project.ts +++ b/app/gui2/src/stores/project.ts @@ -9,6 +9,7 @@ import { createWebsocketClient, rpcWithRetries as lsRpcWithRetries, } from '@/util/net' +import { nextEvent } from '@/util/observable' import { isSome, type Opt } from '@/util/opt' import { tryQualifiedName } from '@/util/qualifiedName' import { VisualizationDataRegistry } from '@/util/visualizationDataRegistry' @@ -518,6 +519,9 @@ export const useProjectStore = defineStore('project', () => { }) } + const firstExecution = lsRpcConnection.then((lsRpc) => + nextEvent(lsRpc, 'executionContext/executionComplete'), + ) const executionContext = createExecutionContextForMain() const computedValueRegistry = ComputedValueRegistry.WithExecutionContext(executionContext) const visualizationDataRegistry = new VisualizationDataRegistry(executionContext, dataConnection) @@ -563,6 +567,7 @@ export const useProjectStore = defineStore('project', () => { }, name: projectName, executionContext, + firstExecution, diagnostics, module, modulePath, diff --git a/app/gui2/src/stores/suggestionDatabase/index.ts b/app/gui2/src/stores/suggestionDatabase/index.ts index 8599001858..3c51f7418c 100644 --- a/app/gui2/src/stores/suggestionDatabase/index.ts +++ b/app/gui2/src/stores/suggestionDatabase/index.ts @@ -76,6 +76,7 @@ class Synchronizer { lsRpc.acquireCapability('search/receivesSuggestionsDatabaseUpdates', {}), ) this.setupUpdateHandler(lsRpc) + this.loadGroups(lsRpc, projectStore.firstExecution) return Synchronizer.loadDatabase(entries, lsRpc, groups.value) }) @@ -125,7 +126,11 @@ class Synchronizer { } }) }) + } + + private async loadGroups(lsRpc: LanguageServer, firstExecution: Promise) { this.queue.pushTask(async ({ currentVersion }) => { + await firstExecution const groups = await lsRpc.getComponentGroups() this.groups.value = groups.componentGroups.map( (group): Group => ({ diff --git a/app/gui2/src/util/events.ts b/app/gui2/src/util/events.ts index 1310f088c7..70416a33e7 100644 --- a/app/gui2/src/util/events.ts +++ b/app/gui2/src/util/events.ts @@ -1,6 +1,5 @@ import type { Opt } from '@/util/opt' import { Vec2 } from '@/util/vec2' -import type { ObservableV2 } from 'lib0/observable' import { computed, onScopeDispose, @@ -12,7 +11,6 @@ import { type Ref, type WatchSource, } from 'vue' -import { ReactiveDb } from './database/reactiveDb' export function isClick(e: MouseEvent | PointerEvent) { if (e instanceof PointerEvent) return e.pointerId !== -1 diff --git a/app/gui2/src/util/getIconName.ts b/app/gui2/src/util/getIconName.ts index 56626b0969..f454181156 100644 --- a/app/gui2/src/util/getIconName.ts +++ b/app/gui2/src/util/getIconName.ts @@ -1,13 +1,17 @@ -import type { SuggestionEntry, Typename } from '@/stores/suggestionDatabase/entry' +import { + SuggestionKind, + type SuggestionEntry, + type Typename, +} from '@/stores/suggestionDatabase/entry' import type { Icon } from '@/util/iconName' import type { MethodPointer } from 'shared/languageServerTypes' -const oldIconNameToNewIconNameLookup: Record = { +const oldIconNameToNewIconNameLookup: Record = { /* eslint-disable camelcase */ - dataframe_clean: 'clean_dataframe', + dataframe_clean: 'table_clean', dataframe_map_row: 'map_row', - dataframe_map_column: 'add_column', - dataframes_join: 'join3', + dataframe_map_column: 'column_add', + dataframes_join: 'join2-1', dataframes_union: 'union', sigma: 'transform4', io: 'in_out', @@ -25,8 +29,8 @@ export function mapOldIconName(oldIconName: string): Icon { const typeNameToIconLookup: Record = { 'Standard.Base.Data.Text.Text': 'text_input', - 'Standard.Base.Data.Numbers.Integer': 'number_input', - 'Standard.Base.Data.Numbers.Float': 'number_input', + 'Standard.Base.Data.Numbers.Integer': 'input_number', + 'Standard.Base.Data.Numbers.Float': 'input_number', 'Standard.Base.Data.Array.Array': 'array_new', 'Standard.Base.Data.Vector.Vector': 'array_new', 'Standard.Base.Data.Time.Date.Date': 'calendar', @@ -43,7 +47,10 @@ export function displayedIconOf( methodCall?: MethodPointer, actualType?: Typename, ): Icon { - if (entry?.iconName) return mapOldIconName(entry.iconName) - if (!methodCall?.name && actualType) return typeNameToIcon(actualType) + if (entry) { + if (entry.iconName) return mapOldIconName(entry.iconName) + if (entry.kind === SuggestionKind.Local) return 'local_scope2' + if (entry.kind === SuggestionKind.Module) return 'collection' + } else if (!methodCall?.name && actualType) return typeNameToIcon(actualType) return 'enso_logo' } diff --git a/app/gui2/src/util/interaction.ts b/app/gui2/src/util/interaction.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/gui2/src/util/observable.ts b/app/gui2/src/util/observable.ts new file mode 100644 index 0000000000..dda8f3575b --- /dev/null +++ b/app/gui2/src/util/observable.ts @@ -0,0 +1,24 @@ +import type { ObservableV2 } from 'lib0/observable' + +export type Events> = O extends ObservableV2 ? E : never + +/** Returns promise which will resolve on the next event. The promise will have the event's + * payload. */ +export function nextEvent, NAME extends string>( + observable: O, + event: NAME, +): Promise[NAME]>> { + type Params = Parameters[NAME]> + return new Promise((resolve) => { + observable.once(event, (...args: Params) => { + resolve(args) + }) + }) +} + +declare const EVENTS_BRAND: unique symbol +declare module 'lib0/observable' { + interface ObservableV2 void }> { + [EVENTS_BRAND]: EVENTS + } +}