mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 06:01:37 +03:00
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:
parent
891f176e9c
commit
48eb173357
@ -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
|
||||
|
||||
|
@ -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', {
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user