CB Fixes and Improvements (#8385)

* The "main view" (when no self type/module nor pattern is specified) contains all methods defined in top modules of every library
* Entries defined inside any "Internal" module from stdlib will be hidden by default; they will be visible only when self type is specified or the module path leading to such module internals.
* Fixed an issue with missing groups sometimes - we must wait with asking for groups for first executionComplete message.
* Modules and local variables have different default icon.

![image](https://github.com/enso-org/enso/assets/3919101/cb33691e-222b-413e-a92e-2cf84e9fe4bb)

![image](https://github.com/enso-org/enso/assets/3919101/beab202d-4feb-4b00-ba0c-c141862da53c)
This commit is contained in:
Adam Obuchowicz 2023-11-28 11:38:28 +01:00 committed by GitHub
parent c889c8e83f
commit a38680adf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1478 additions and 1128 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

@ -353,7 +353,7 @@ const handler = componentBrowserBindings.handler({
<div class="top-bar"> <div class="top-bar">
<div class="top-bar-inner"> <div class="top-bar-inner">
<ToggleIcon v-model="filterFlags.showLocal" icon="local_scope2" /> <ToggleIcon v-model="filterFlags.showLocal" icon="local_scope2" />
<ToggleIcon icon="command_key3" /> <ToggleIcon icon="command3" />
<ToggleIcon v-model="filterFlags.showUnstable" icon="unstable2" /> <ToggleIcon v-model="filterFlags.showUnstable" icon="unstable2" />
<ToggleIcon icon="marketplace" /> <ToggleIcon icon="marketplace" />
<ToggleIcon v-model="docsVisible" icon="right_side_panel" class="first-on-right" /> <ToggleIcon v-model="docsVisible" icon="right_side_panel" class="first-on-right" />

View File

@ -20,15 +20,21 @@ test.each([
{ ...makeStaticMethod('Standard.Base.Data.Vector.Vector.new'), groupIndex: 1 }, { ...makeStaticMethod('Standard.Base.Data.Vector.Vector.new'), groupIndex: 1 },
makeModule('local.New_Project'), makeModule('local.New_Project'),
makeModule('Standard.Base.Data'), 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) => { ])('$name entry is in the CB main view', (entry) => {
const filtering = new Filtering({}) const filtering = new Filtering({})
expect(filtering.filter(entry)).not.toBeNull() expect(filtering.filter(entry)).not.toBeNull()
}) })
test.each([ 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 { ...makeMethod('Standard.Base.Data.Vector.Vector.get'), groupIndex: 1 }, // not static method
makeModule('Standard.Base.Data.Vector'), // Not top module 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) => { ])('$name entry is not in the CB main view', (entry) => {
const filtering = new Filtering({}) const filtering = new Filtering({})
expect(filtering.filter(entry)).toBeNull() expect(filtering.filter(entry)).toBeNull()
@ -63,6 +69,18 @@ test.each([
expect(substringFiltering.filter(entry)).toBeNull() 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([ test.each([
makeModuleMethod('local.Project.Module.foo'), makeModuleMethod('local.Project.Module.foo'),
makeModuleMethod('local.Project.Module.Submodule.foo_in_submodule'), makeModuleMethod('local.Project.Module.Submodule.foo_in_submodule'),

View File

@ -285,6 +285,7 @@ export class Filtering {
showUnstable: boolean = false showUnstable: boolean = false
showLocal: boolean = false showLocal: boolean = false
currentModule?: QualifiedName currentModule?: QualifiedName
browsingInternalModule: boolean = false
constructor(filter: Filter, currentModule: Opt<QualifiedName> = undefined) { constructor(filter: Filter, currentModule: Opt<QualifiedName> = undefined) {
const { pattern, selfArg, qualifiedNamePattern, showUnstable, showLocal } = filter const { pattern, selfArg, qualifiedNamePattern, showUnstable, showLocal } = filter
@ -295,6 +296,7 @@ export class Filtering {
if (qualifiedNamePattern) { if (qualifiedNamePattern) {
this.qualifiedName = new FilteringQualifiedName(qualifiedNamePattern) this.qualifiedName = new FilteringQualifiedName(qualifiedNamePattern)
this.fullPattern = pattern ? `${qualifiedNamePattern}.${pattern}` : qualifiedNamePattern this.fullPattern = pattern ? `${qualifiedNamePattern}.${pattern}` : qualifiedNamePattern
this.browsingInternalModule = isInternalModulePath(qualifiedNamePattern)
} else if (pattern) this.fullPattern = pattern } else if (pattern) this.fullPattern = pattern
if (this.fullPattern) { if (this.fullPattern) {
let prefix = '' let prefix = ''
@ -334,9 +336,11 @@ export class Filtering {
private mainViewFilter(entry: SuggestionEntry): MatchResult | null { private mainViewFilter(entry: SuggestionEntry): MatchResult | null {
const hasGroup = entry.groupIndex != null const hasGroup = entry.groupIndex != null
const isModule = entry.kind === SuggestionKind.Module const isModule = entry.kind === SuggestionKind.Module
const isTopElement = qnIsTopElement(entry.definedIn) const isMethod = entry.kind === SuggestionKind.Method
if (!hasGroup && (!isModule || !isTopElement)) return null const isInTopModule = qnIsTopElement(entry.definedIn)
else return { score: 0 } const isTopElementInMainView = (isMethod || isModule) && isInTopModule
if (hasGroup || isTopElementInMainView) return { score: 0 }
else return null
} }
private isLocal(entry: SuggestionEntry): boolean { private isLocal(entry: SuggestionEntry): boolean {
@ -344,18 +348,28 @@ export class Filtering {
} }
filter(entry: SuggestionEntry): MatchResult | null { filter(entry: SuggestionEntry): MatchResult | null {
let qualifiedNameMatch: Opt<MatchedParts>
if (entry.isPrivate) return null if (entry.isPrivate) return null
else if (!this.selfTypeMatches(entry)) return null if (this.selfArg == null && isInternal(entry) && !this.browsingInternalModule) return null
else if (!(qualifiedNameMatch = this.qualifiedNameMatches(entry))) return null if (!this.selfTypeMatches(entry)) return null
else if (!this.showUnstable && entry.isUnstable) return null const qualifiedNameMatch = this.qualifiedNameMatches(entry)
else if (this.showLocal && !this.isLocal(entry)) return null if (!qualifiedNameMatch) return null
else if (this.pattern) { if (!this.showUnstable && entry.isUnstable) return null
if (this.showLocal && !this.isLocal(entry)) return null
if (this.pattern) {
const patternMatch = this.pattern.tryMatch(entry) const patternMatch = this.pattern.tryMatch(entry)
if (!patternMatch) return null if (!patternMatch) return null
if (!this.showLocal && this.isLocal(entry)) patternMatch.score *= 2 if (!this.showLocal && this.isLocal(entry)) patternMatch.score *= 2
return { ...qualifiedNameMatch, ...patternMatch } 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)
}

View File

@ -9,6 +9,7 @@ import {
createWebsocketClient, createWebsocketClient,
rpcWithRetries as lsRpcWithRetries, rpcWithRetries as lsRpcWithRetries,
} from '@/util/net' } from '@/util/net'
import { nextEvent } from '@/util/observable'
import { isSome, type Opt } from '@/util/opt' import { isSome, type Opt } from '@/util/opt'
import { tryQualifiedName } from '@/util/qualifiedName' import { tryQualifiedName } from '@/util/qualifiedName'
import { VisualizationDataRegistry } from '@/util/visualizationDataRegistry' 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 executionContext = createExecutionContextForMain()
const computedValueRegistry = ComputedValueRegistry.WithExecutionContext(executionContext) const computedValueRegistry = ComputedValueRegistry.WithExecutionContext(executionContext)
const visualizationDataRegistry = new VisualizationDataRegistry(executionContext, dataConnection) const visualizationDataRegistry = new VisualizationDataRegistry(executionContext, dataConnection)
@ -563,6 +567,7 @@ export const useProjectStore = defineStore('project', () => {
}, },
name: projectName, name: projectName,
executionContext, executionContext,
firstExecution,
diagnostics, diagnostics,
module, module,
modulePath, modulePath,

View File

@ -76,6 +76,7 @@ class Synchronizer {
lsRpc.acquireCapability('search/receivesSuggestionsDatabaseUpdates', {}), lsRpc.acquireCapability('search/receivesSuggestionsDatabaseUpdates', {}),
) )
this.setupUpdateHandler(lsRpc) this.setupUpdateHandler(lsRpc)
this.loadGroups(lsRpc, projectStore.firstExecution)
return Synchronizer.loadDatabase(entries, lsRpc, groups.value) return Synchronizer.loadDatabase(entries, lsRpc, groups.value)
}) })
@ -125,7 +126,11 @@ class Synchronizer {
} }
}) })
}) })
}
private async loadGroups(lsRpc: LanguageServer, firstExecution: Promise<unknown>) {
this.queue.pushTask(async ({ currentVersion }) => { this.queue.pushTask(async ({ currentVersion }) => {
await firstExecution
const groups = await lsRpc.getComponentGroups() const groups = await lsRpc.getComponentGroups()
this.groups.value = groups.componentGroups.map( this.groups.value = groups.componentGroups.map(
(group): Group => ({ (group): Group => ({

View File

@ -1,6 +1,5 @@
import type { Opt } from '@/util/opt' import type { Opt } from '@/util/opt'
import { Vec2 } from '@/util/vec2' import { Vec2 } from '@/util/vec2'
import type { ObservableV2 } from 'lib0/observable'
import { import {
computed, computed,
onScopeDispose, onScopeDispose,
@ -12,7 +11,6 @@ import {
type Ref, type Ref,
type WatchSource, type WatchSource,
} from 'vue' } from 'vue'
import { ReactiveDb } from './database/reactiveDb'
export function isClick(e: MouseEvent | PointerEvent) { export function isClick(e: MouseEvent | PointerEvent) {
if (e instanceof PointerEvent) return e.pointerId !== -1 if (e instanceof PointerEvent) return e.pointerId !== -1

View File

@ -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 { Icon } from '@/util/iconName'
import type { MethodPointer } from 'shared/languageServerTypes' import type { MethodPointer } from 'shared/languageServerTypes'
const oldIconNameToNewIconNameLookup: Record<string, string> = { const oldIconNameToNewIconNameLookup: Record<string, Icon> = {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
dataframe_clean: 'clean_dataframe', dataframe_clean: 'table_clean',
dataframe_map_row: 'map_row', dataframe_map_row: 'map_row',
dataframe_map_column: 'add_column', dataframe_map_column: 'column_add',
dataframes_join: 'join3', dataframes_join: 'join2-1',
dataframes_union: 'union', dataframes_union: 'union',
sigma: 'transform4', sigma: 'transform4',
io: 'in_out', io: 'in_out',
@ -25,8 +29,8 @@ export function mapOldIconName(oldIconName: string): Icon {
const typeNameToIconLookup: Record<string, Icon> = { const typeNameToIconLookup: Record<string, Icon> = {
'Standard.Base.Data.Text.Text': 'text_input', 'Standard.Base.Data.Text.Text': 'text_input',
'Standard.Base.Data.Numbers.Integer': 'number_input', 'Standard.Base.Data.Numbers.Integer': 'input_number',
'Standard.Base.Data.Numbers.Float': 'number_input', 'Standard.Base.Data.Numbers.Float': 'input_number',
'Standard.Base.Data.Array.Array': 'array_new', 'Standard.Base.Data.Array.Array': 'array_new',
'Standard.Base.Data.Vector.Vector': 'array_new', 'Standard.Base.Data.Vector.Vector': 'array_new',
'Standard.Base.Data.Time.Date.Date': 'calendar', 'Standard.Base.Data.Time.Date.Date': 'calendar',
@ -43,7 +47,10 @@ export function displayedIconOf(
methodCall?: MethodPointer, methodCall?: MethodPointer,
actualType?: Typename, actualType?: Typename,
): Icon { ): Icon {
if (entry?.iconName) return mapOldIconName(entry.iconName) if (entry) {
if (!methodCall?.name && actualType) return typeNameToIcon(actualType) 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' return 'enso_logo'
} }

View File

@ -0,0 +1,24 @@
import type { ObservableV2 } from 'lib0/observable'
export type Events<O extends ObservableV2<any>> = O extends ObservableV2<infer E> ? E : never
/** Returns promise which will resolve on the next event. The promise will have the event's
* payload. */
export function nextEvent<O extends ObservableV2<any>, NAME extends string>(
observable: O,
event: NAME,
): Promise<Parameters<Events<O>[NAME]>> {
type Params = Parameters<Events<O>[NAME]>
return new Promise<Params>((resolve) => {
observable.once(event, (...args: Params) => {
resolve(args)
})
})
}
declare const EVENTS_BRAND: unique symbol
declare module 'lib0/observable' {
interface ObservableV2<EVENTS extends { [key: string]: (...arg0: any[]) => void }> {
[EVENTS_BRAND]: EVENTS
}
}