External documentation (#10396)

- Button shown in CB and circular menu opens documentation in new window.
- F1 key opens documentation for selected node.

https://github.com/enso-org/enso/assets/1047859/24d7e8cc-3f5c-4644-9d11-7deb5d1a8767

Closes #10276.
This commit is contained in:
Kaz Wesley 2024-07-04 09:01:00 -07:00 committed by GitHub
parent 891f176e9c
commit 48eb173357
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 129 additions and 51 deletions

View File

@ -39,6 +39,7 @@
index column will select row or value in seperate node
- [Copied table-viz range pastes as Table component][10352]
- [Added support for links in documentation panels][10353].
- [Added support for opening documentation in an external browser][10396].
[10064]: https://github.com/enso-org/enso/pull/10064
[10179]: https://github.com/enso-org/enso/pull/10179
@ -53,6 +54,7 @@
[10340]: https://github.com/enso-org/enso/pull/10340
[10352]: https://github.com/enso-org/enso/pull/10352
[10353]: https://github.com/enso-org/enso/pull/10353
[10396]: https://github.com/enso-org/enso/pull/10396
#### Enso Standard Library

View File

@ -42,6 +42,7 @@ export const graphBindings = defineKeybinds('graph-editor', {
enterNode: ['Mod+E'],
exitNode: ['Mod+Shift+E'],
changeColorSelectedNodes: ['Mod+Shift+C'],
openDocumentation: ['F1'],
})
export const visualizationBindings = defineKeybinds('visualization', {

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { graphBindings } from '@/bindings'
import ColorRing from '@/components/ColorRing.vue'
import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation'
import SmallPlusButton from '@/components/SmallPlusButton.vue'
@ -14,6 +15,7 @@ const props = defineProps<{
isVisualizationEnabled: boolean
isFullMenuVisible: boolean
matchableNodeColors: Set<string>
documentationUrl: string | undefined
}>()
const emit = defineEmits<{
'update:isRecordingOverridden': [isRecordingOverridden: boolean]
@ -27,37 +29,51 @@ const emit = defineEmits<{
}>()
const showColorPicker = ref(false)
function openDocs(url: string) {
window.open(url, '_blank')
}
function readableBinding(binding: keyof (typeof graphBindings)['bindings']) {
return graphBindings.bindings[binding].humanReadable
}
</script>
<template>
<div class="CircularMenu" :class="{ partial: !props.isFullMenuVisible }">
<div class="CircularMenu">
<div
v-if="!showColorPicker"
class="circle menu"
:class="`${props.isFullMenuVisible ? 'full' : 'partial'}`"
>
<div v-if="!isFullMenuVisible" class="More" @pointerdown.stop="emit('openFullMenu')"></div>
<SvgButton
v-if="isFullMenuVisible"
name="comment"
class="slot2"
title="Comment"
@click.stop="emit('startEditingComment')"
/>
<SvgButton
v-if="isFullMenuVisible"
name="paint_palette"
class="slot3"
title="Color"
@click.stop="showColorPicker = true"
/>
<SvgButton
v-if="isFullMenuVisible"
name="trash2"
class="slot4"
title="Delete"
@click.stop="emit('delete')"
/>
<template v-if="isFullMenuVisible">
<SvgButton
v-if="documentationUrl"
name="help"
class="slot1"
:title="`Open Documentation (${readableBinding('openDocumentation')})`"
@click.stop="openDocs(documentationUrl)"
/>
<SvgButton
name="comment"
class="slot2"
title="Comment"
@click.stop="emit('startEditingComment')"
/>
<SvgButton
name="paint_palette"
class="slot3"
title="Color"
@click.stop="showColorPicker = true"
/>
<SvgButton
name="trash2"
class="slot4"
:title="`Delete (${readableBinding('deleteSelected')})`"
@click.stop="emit('delete')"
/>
</template>
<div v-else class="More" @pointerdown.stop="emit('openFullMenu')"></div>
<ToggleIcon
icon="eye"
class="slot5"

View File

@ -9,9 +9,11 @@ import DocsTags from '@/components/DocumentationPanel/DocsTags.vue'
import { HistoryStack } from '@/components/DocumentationPanel/history'
import type { Docs, FunctionDocs, Sections, TypeDocs } from '@/components/DocumentationPanel/ir'
import { lookupDocumentation, placeholder } from '@/components/DocumentationPanel/ir'
import SvgButton from '@/components/SvgButton.vue'
import { groupColorStyle } from '@/composables/nodeColors'
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
import type { SuggestionId } from '@/stores/suggestionDatabase/entry'
import { suggestionDocumentationUrl } from '@/stores/suggestionDatabase/entry'
import { tryGetIndex } from '@/util/data/array'
import { type Opt } from '@/util/data/opt'
import type { Icon as IconName } from '@/util/iconName'
@ -60,21 +62,17 @@ const name = computed<Opt<QualifiedName>>(() => {
// === Breadcrumbs ===
const color = computed(() => {
const groupIndex =
props.selectedEntry != null ? db.entries.get(props.selectedEntry)?.groupIndex : undefined
return groupColorStyle(tryGetIndex(db.groups, groupIndex))
})
const suggestion = computed(() =>
props.selectedEntry != null ? db.entries.get(props.selectedEntry) : undefined,
)
const icon = computed<IconName>(() => {
const id = props.selectedEntry
if (id) {
const entry = db.entries.get(id)
return entry?.iconName ?? 'marketplace'
} else {
return 'marketplace'
}
})
const color = computed(() => groupColorStyle(tryGetIndex(db.groups, suggestion.value?.groupIndex)))
const icon = computed<IconName>(() => suggestion.value?.iconName ?? 'marketplace')
const documentationUrl = computed(
() => suggestion.value && suggestionDocumentationUrl(suggestion.value),
)
const historyStack = new HistoryStack()
@ -115,21 +113,32 @@ function handleBreadcrumbClick(index: number) {
}
}
}
function openDocs(url: string) {
window.open(url, '_blank')
}
</script>
<template>
<div class="DocumentationPanel scrollable" @wheel.stop.passive>
<Breadcrumbs
v-if="!isPlaceholder"
:breadcrumbs="breadcrumbs"
:color="color"
:icon="icon"
:canGoForward="historyStack.canGoForward()"
:canGoBackward="historyStack.canGoBackward()"
@click="(index) => handleBreadcrumbClick(index)"
@forward="historyStack.forward()"
@backward="historyStack.backward()"
/>
<div v-if="!isPlaceholder" class="topBar">
<Breadcrumbs
:breadcrumbs="breadcrumbs"
:color="color"
:icon="icon"
:canGoForward="historyStack.canGoForward()"
:canGoBackward="historyStack.canGoBackward()"
@click="(index) => handleBreadcrumbClick(index)"
@forward="historyStack.forward()"
@backward="historyStack.backward()"
/>
<SvgButton
v-if="documentationUrl"
name="open"
title="Open in New Window"
@click.stop="openDocs(documentationUrl)"
/>
</div>
<DocsTags
v-if="sections.tags.length > 0"
class="tags"
@ -198,4 +207,12 @@ function handleBreadcrumbClick(index: number) {
width: 100%;
padding: 0 8px;
}
.topBar {
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -42,7 +42,7 @@ import { asNodeId } from '@/stores/graph/graphDatabase'
import type { RequiredImport } from '@/stores/graph/imports'
import { useProjectStore } from '@/stores/project'
import { provideSuggestionDbStore } from '@/stores/suggestionDatabase'
import type { Typename } from '@/stores/suggestionDatabase/entry'
import { suggestionDocumentationUrl, type Typename } from '@/stores/suggestionDatabase/entry'
import { provideVisualizationStore } from '@/stores/visualization'
import { bail } from '@/util/assert'
import type { AstId } from '@/util/ast/abstract'
@ -349,8 +349,30 @@ const graphBindingsHandler = graphBindings.handler({
changeColorSelectedNodes() {
showColorPicker.value = true
},
openDocumentation() {
const failure = 'Unable to show node documentation'
const selected = getSoleSelectionOrToast(failure)
if (selected == null) return
const suggestion = graphStore.db.nodeMainSuggestion.lookup(selected)
const documentation = suggestion && suggestionDocumentationUrl(suggestion)
if (documentation) {
window.open(documentation, '_blank')
} else {
toasts.userActionFailed.show(`${failure}: no documentation available for selected node.`)
}
},
})
function getSoleSelectionOrToast(context: string) {
if (nodeSelection.selected.size === 0) {
toasts.userActionFailed.show(`${context}: no node selected.`)
} else if (nodeSelection.selected.size > 1) {
toasts.userActionFailed.show(`${context}: multiple nodes selected.`)
} else {
return set.first(nodeSelection.selected)
}
}
const { handleClick } = useDoubleClick(
(e: MouseEvent) => {
if (e.target !== e.currentTarget) return false

View File

@ -27,6 +27,7 @@ import { injectGraphSelection } from '@/providers/graphSelection'
import { useGraphStore, type Node } from '@/stores/graph'
import { asNodeId } from '@/stores/graph/graphDatabase'
import { useProjectStore } from '@/stores/project'
import { suggestionDocumentationUrl } from '@/stores/suggestionDatabase/entry'
import { Ast } from '@/util/ast'
import type { AstId } from '@/util/ast/abstract'
import { prefixes } from '@/util/ast/node'
@ -318,6 +319,9 @@ const icon = computed(() => {
outputPortLabel.value,
)
})
const documentationUrl = computed(
() => suggestionEntry.value && suggestionDocumentationUrl(suggestionEntry.value),
)
const nodeEditHandler = nodeEditBindings.handler({
cancel(e) {
@ -470,6 +474,7 @@ watchEffect(() => {
:isFullMenuVisible="menuVisible && menuFull"
:nodeColor="getNodeColor(nodeId)"
:matchableNodeColors="matchableNodeColors"
:documentationUrl="documentationUrl"
@update:isVisualizationEnabled="emit('update:visualizationEnabled', $event)"
@startEditing="startEditingNode"
@startEditingComment="editingComment = true"

View File

@ -1,15 +1,15 @@
import { assert } from '@/util/assert'
import type { Doc } from '@/util/docParser'
import type { Icon } from '@/util/iconName'
import type { IdentifierOrOperatorIdentifier, QualifiedName } from '@/util/qualifiedName'
import {
isIdentifierOrOperatorIdentifier,
isQualifiedName,
qnJoin,
qnLastSegment,
qnParent,
qnSegments,
qnSplit,
type IdentifierOrOperatorIdentifier,
type QualifiedName,
} from '@/util/qualifiedName'
import type { MethodPointer } from 'shared/languageServerTypes'
import type {
@ -94,6 +94,21 @@ export function entryMethodPointer(entry: SuggestionEntry): MethodPointer | unde
}
}
const DOCUMENTATION_ROOT = 'https://help.enso.org/docs/api'
export function suggestionDocumentationUrl(entry: SuggestionEntry): string | undefined {
if (entry.kind !== SuggestionKind.Method && entry.kind !== SuggestionKind.Function) return
const location = entry.memberOf ?? entry.definedIn
const segments: string[] = qnSegments(location)
if (segments[0] !== 'Standard') return
if (segments.length < 3) return
const namespace = segments[0]
segments[0] = DOCUMENTATION_ROOT
segments[1] = `${namespace}.${segments[1]}`
segments[segments.length - 1] += `.${entry.name}`
return segments.join('/')
}
function makeSimpleEntry(
kind: SuggestionKind,
definedIn: QualifiedName,