mirror of
https://github.com/enso-org/enso.git
synced 2025-01-09 03:37:19 +03:00
Renaming project in GUI (#10243)
Fixes #10073 [Screencast from 2024-06-11 13-01-10.webm](https://github.com/enso-org/enso/assets/3919101/2917ad41-e080-482b-9a69-690e80087132)
This commit is contained in:
parent
5fd0e0c30d
commit
4164277d22
@ -9,6 +9,7 @@
|
|||||||
- [Copy-pasting multiple nodes][10194].
|
- [Copy-pasting multiple nodes][10194].
|
||||||
- The documentation editor has [formatting toolbars][10064].
|
- The documentation editor has [formatting toolbars][10064].
|
||||||
- The documentation editor supports [rendering images][10205].
|
- The documentation editor supports [rendering images][10205].
|
||||||
|
- [Project may be renamed in Project View][10243]
|
||||||
- [Fixed a bug where drop-down were not displayed for some arguments][10297].
|
- [Fixed a bug where drop-down were not displayed for some arguments][10297].
|
||||||
For example, `locale` parameter of `Equal_Ignore_Case` kind in join component.
|
For example, `locale` parameter of `Equal_Ignore_Case` kind in join component.
|
||||||
- [Node previews][10310]: Node may be previewed by hovering output port while
|
- [Node previews][10310]: Node may be previewed by hovering output port while
|
||||||
@ -19,6 +20,7 @@
|
|||||||
[10194]: https://github.com/enso-org/enso/pull/10194
|
[10194]: https://github.com/enso-org/enso/pull/10194
|
||||||
[10198]: https://github.com/enso-org/enso/pull/10198
|
[10198]: https://github.com/enso-org/enso/pull/10198
|
||||||
[10205]: https://github.com/enso-org/enso/pull/10205
|
[10205]: https://github.com/enso-org/enso/pull/10205
|
||||||
|
[10243]: https://github.com/enso-org/enso/pull/10243
|
||||||
[10297]: https://github.com/enso-org/enso/pull/10297
|
[10297]: https://github.com/enso-org/enso/pull/10297
|
||||||
[10310]: https://github.com/enso-org/enso/pull/10310
|
[10310]: https://github.com/enso-org/enso/pull/10310
|
||||||
|
|
||||||
|
@ -479,6 +479,11 @@ export class LanguageServer extends ObservableV2<Notifications & TransportEvents
|
|||||||
return this.request('refactoring/renameSymbol', { module, expressionId, newName })
|
return this.request('refactoring/renameSymbol', { module, expressionId, newName })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#refactoringrenameproject) */
|
||||||
|
renameProject(namespace: string, oldName: string, newName: string): Promise<LsRpcResult<void>> {
|
||||||
|
return this.request('refactoring/renameProject', { namespace, oldName, newName })
|
||||||
|
}
|
||||||
|
|
||||||
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#profilingstart) */
|
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#profilingstart) */
|
||||||
profilingStart(memorySnapshot?: boolean): Promise<LsRpcResult<void>> {
|
profilingStart(memorySnapshot?: boolean): Promise<LsRpcResult<void>> {
|
||||||
return this.request('profiling/start', { memorySnapshot })
|
return this.request('profiling/start', { memorySnapshot })
|
||||||
|
@ -343,7 +343,11 @@ export type Notifications = {
|
|||||||
'file/event': (param: { path: Path; kind: FileEventKind }) => void
|
'file/event': (param: { path: Path; kind: FileEventKind }) => void
|
||||||
'file/rootAdded': (param: {}) => void
|
'file/rootAdded': (param: {}) => void
|
||||||
'file/rootRemoved': (param: {}) => void
|
'file/rootRemoved': (param: {}) => void
|
||||||
'refactoring/projectRenamed': (param: {}) => void
|
'refactoring/projectRenamed': (param: {
|
||||||
|
oldNormalizedName: string
|
||||||
|
newNormalizedName: string
|
||||||
|
newName: string
|
||||||
|
}) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Event<T extends keyof Notifications> = Parameters<Notifications[T]>[0]
|
export type Event<T extends keyof Notifications> = Parameters<Notifications[T]>[0]
|
||||||
|
@ -25,6 +25,7 @@ const props = defineProps<{
|
|||||||
logEvent: LogEvent
|
logEvent: LogEvent
|
||||||
hidden: boolean
|
hidden: boolean
|
||||||
ignoreParamsRegex?: RegExp
|
ignoreParamsRegex?: RegExp
|
||||||
|
renameProject: (newName: string) => void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const classSet = provideAppClassSet()
|
const classSet = provideAppClassSet()
|
||||||
@ -71,7 +72,13 @@ registerAutoBlurHandler()
|
|||||||
:unrecognizedOptions="appConfig.unrecognizedOptions"
|
:unrecognizedOptions="appConfig.unrecognizedOptions"
|
||||||
:config="appConfig.config"
|
:config="appConfig.config"
|
||||||
/>
|
/>
|
||||||
<ProjectView v-else v-show="!props.hidden" class="App" :class="[...classSet.keys()]" />
|
<ProjectView
|
||||||
|
v-else
|
||||||
|
v-show="!props.hidden"
|
||||||
|
class="App"
|
||||||
|
:class="[...classSet.keys()]"
|
||||||
|
:renameProject="renameProject"
|
||||||
|
/>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<TooltipDisplayer :registry="appTooltips" />
|
<TooltipDisplayer :registry="appTooltips" />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
@ -28,19 +28,19 @@ import { useDoubleClick } from '@/composables/doubleClick'
|
|||||||
import { keyboardBusy, keyboardBusyExceptIn, unrefElement, useEvent } from '@/composables/events'
|
import { keyboardBusy, keyboardBusyExceptIn, unrefElement, useEvent } from '@/composables/events'
|
||||||
import { groupColorVar } from '@/composables/nodeColors'
|
import { groupColorVar } from '@/composables/nodeColors'
|
||||||
import type { PlacementStrategy } from '@/composables/nodeCreation'
|
import type { PlacementStrategy } from '@/composables/nodeCreation'
|
||||||
import { useStackNavigator } from '@/composables/stackNavigator'
|
|
||||||
import { useSyncLocalStorage } from '@/composables/syncLocalStorage'
|
import { useSyncLocalStorage } from '@/composables/syncLocalStorage'
|
||||||
import { provideGraphNavigator, type GraphNavigator } from '@/providers/graphNavigator'
|
import { provideGraphNavigator, type GraphNavigator } from '@/providers/graphNavigator'
|
||||||
import { provideNodeColors } from '@/providers/graphNodeColors'
|
import { provideNodeColors } from '@/providers/graphNodeColors'
|
||||||
import { provideNodeCreation } from '@/providers/graphNodeCreation'
|
import { provideNodeCreation } from '@/providers/graphNodeCreation'
|
||||||
import { provideGraphSelection } from '@/providers/graphSelection'
|
import { provideGraphSelection } from '@/providers/graphSelection'
|
||||||
|
import { provideStackNavigator } from '@/providers/graphStackNavigator'
|
||||||
import { provideInteractionHandler } from '@/providers/interactionHandler'
|
import { provideInteractionHandler } from '@/providers/interactionHandler'
|
||||||
import { provideKeyboard } from '@/providers/keyboard'
|
import { provideKeyboard } from '@/providers/keyboard'
|
||||||
import { provideWidgetRegistry } from '@/providers/widgetRegistry'
|
import { provideWidgetRegistry } from '@/providers/widgetRegistry'
|
||||||
import { provideGraphStore, type NodeId } from '@/stores/graph'
|
import { provideGraphStore, type NodeId } from '@/stores/graph'
|
||||||
import { asNodeId } from '@/stores/graph/graphDatabase'
|
import { asNodeId } from '@/stores/graph/graphDatabase'
|
||||||
import type { RequiredImport } from '@/stores/graph/imports'
|
import type { RequiredImport } from '@/stores/graph/imports'
|
||||||
import { provideProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
import { provideSuggestionDbStore } from '@/stores/suggestionDatabase'
|
import { provideSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||||
import type { Typename } from '@/stores/suggestionDatabase/entry'
|
import type { Typename } from '@/stores/suggestionDatabase/entry'
|
||||||
import { provideVisualizationStore } from '@/stores/visualization'
|
import { provideVisualizationStore } from '@/stores/visualization'
|
||||||
@ -70,7 +70,7 @@ import {
|
|||||||
} from 'vue'
|
} from 'vue'
|
||||||
|
|
||||||
const keyboard = provideKeyboard()
|
const keyboard = provideKeyboard()
|
||||||
const projectStore = provideProjectStore()
|
const projectStore = useProjectStore()
|
||||||
const suggestionDb = provideSuggestionDbStore(projectStore)
|
const suggestionDb = provideSuggestionDbStore(projectStore)
|
||||||
const graphStore = provideGraphStore(projectStore, suggestionDb)
|
const graphStore = provideGraphStore(projectStore, suggestionDb)
|
||||||
const widgetRegistry = provideWidgetRegistry(graphStore.db)
|
const widgetRegistry = provideWidgetRegistry(graphStore.db)
|
||||||
@ -194,7 +194,7 @@ function panToSelected() {
|
|||||||
|
|
||||||
// == Breadcrumbs ==
|
// == Breadcrumbs ==
|
||||||
|
|
||||||
const stackNavigator = useStackNavigator(projectStore, graphStore)
|
const stackNavigator = provideStackNavigator(projectStore, graphStore)
|
||||||
|
|
||||||
// === Toasts ===
|
// === Toasts ===
|
||||||
|
|
||||||
@ -706,14 +706,8 @@ const groupColors = computed(() => {
|
|||||||
v-model:showColorPicker="showColorPicker"
|
v-model:showColorPicker="showColorPicker"
|
||||||
v-model:showCodeEditor="showCodeEditor"
|
v-model:showCodeEditor="showCodeEditor"
|
||||||
v-model:showDocumentationEditor="showDocumentationEditor"
|
v-model:showDocumentationEditor="showDocumentationEditor"
|
||||||
:breadcrumbs="stackNavigator.breadcrumbLabels.value"
|
|
||||||
:allowNavigationLeft="stackNavigator.allowNavigationLeft.value"
|
|
||||||
:allowNavigationRight="stackNavigator.allowNavigationRight.value"
|
|
||||||
:zoomLevel="100.0 * graphNavigator.targetScale"
|
:zoomLevel="100.0 * graphNavigator.targetScale"
|
||||||
:componentsSelected="nodeSelection.selected.size"
|
:componentsSelected="nodeSelection.selected.size"
|
||||||
@breadcrumbClick="stackNavigator.handleBreadcrumbClick"
|
|
||||||
@back="stackNavigator.exitNode"
|
|
||||||
@forward="stackNavigator.enterNextNodeFromHistory"
|
|
||||||
@recordOnce="onRecordOnceButtonPress()"
|
@recordOnce="onRecordOnceButtonPress()"
|
||||||
@fitToAllClicked="zoomToSelected"
|
@fitToAllClicked="zoomToSelected"
|
||||||
@zoomIn="graphNavigator.stepZoom(+1)"
|
@zoomIn="graphNavigator.stepZoom(+1)"
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NavBreadcrumbs, { type BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
|
import NavBreadcrumbs from '@/components/NavBreadcrumbs.vue'
|
||||||
import SvgIcon from '@/components/SvgIcon.vue'
|
import SvgIcon from '@/components/SvgIcon.vue'
|
||||||
|
import { injectStackNavigator } from '@/providers/graphStackNavigator'
|
||||||
import SvgButton from './SvgButton.vue'
|
import SvgButton from './SvgButton.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const stackNavigator = injectStackNavigator()
|
||||||
breadcrumbs: BreadcrumbItem[]
|
|
||||||
allowNavigationLeft: boolean
|
|
||||||
allowNavigationRight: boolean
|
|
||||||
}>()
|
|
||||||
const emit = defineEmits<{ back: []; forward: []; breadcrumbClick: [index: number] }>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -17,18 +13,18 @@ const emit = defineEmits<{ back: []; forward: []; breadcrumbClick: [index: numbe
|
|||||||
<div class="breadcrumbs-controls">
|
<div class="breadcrumbs-controls">
|
||||||
<SvgButton
|
<SvgButton
|
||||||
name="arrow_left"
|
name="arrow_left"
|
||||||
:disabled="!props.allowNavigationLeft"
|
:disabled="!stackNavigator.allowNavigationLeft"
|
||||||
title="Back"
|
title="Back"
|
||||||
@click.stop="emit('back')"
|
@click.stop="stackNavigator.exitNode"
|
||||||
/>
|
/>
|
||||||
<SvgButton
|
<SvgButton
|
||||||
name="arrow_right"
|
name="arrow_right"
|
||||||
:disabled="!props.allowNavigationRight"
|
:disabled="!stackNavigator.allowNavigationRight"
|
||||||
title="Forward"
|
title="Forward"
|
||||||
@click.stop="emit('forward')"
|
@click.stop="stackNavigator.enterNextNodeFromHistory"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NavBreadcrumbs :breadcrumbs="props.breadcrumbs" @selected="emit('breadcrumbClick', $event)" />
|
<NavBreadcrumbs />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{ text: string; active: boolean }>()
|
import { ref, watch, type ComponentInstance } from 'vue'
|
||||||
|
import AutoSizedInput from './widgets/AutoSizedInput.vue'
|
||||||
|
|
||||||
|
const model = defineModel<string>({ required: true })
|
||||||
|
const _props = defineProps<{ active: boolean; editing: boolean }>()
|
||||||
|
|
||||||
|
const input = ref<ComponentInstance<typeof AutoSizedInput>>()
|
||||||
|
watch(input, (input, old) => {
|
||||||
|
if (old == null && input != null) input.focus()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['NavBreadcrumb', { inactive: !props.active }]">
|
<div :class="['NavBreadcrumb', { inactive: !active }]">
|
||||||
<span v-text="props.text"></span>
|
<AutoSizedInput v-if="editing" ref="input" v-model.lazy="model" :autoSelect="true" />
|
||||||
|
<span v-else v-text="model"></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,31 +1,50 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NavBreadcrumb from '@/components/NavBreadcrumb.vue'
|
import NavBreadcrumb from '@/components/NavBreadcrumb.vue'
|
||||||
import SvgButton from '@/components/SvgButton.vue'
|
import SvgButton from '@/components/SvgButton.vue'
|
||||||
|
import { injectStackNavigator } from '@/providers/graphStackNavigator'
|
||||||
|
import { useProjectStore } from '@/stores/project'
|
||||||
|
import { useToast } from '@/util/toast'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export interface BreadcrumbItem {
|
export interface BreadcrumbItem {
|
||||||
label: string
|
label: string
|
||||||
active: boolean
|
active: boolean
|
||||||
}
|
}
|
||||||
|
const renameError = useToast.error()
|
||||||
|
const projectNameEdited = ref(false)
|
||||||
|
|
||||||
const props = defineProps<{ breadcrumbs: BreadcrumbItem[] }>()
|
const stackNavigator = injectStackNavigator()
|
||||||
const emit = defineEmits<{ selected: [index: number] }>()
|
const project = useProjectStore()
|
||||||
|
|
||||||
|
async function renameBreadcrumb(index: number, newName: string) {
|
||||||
|
if (index === 0) {
|
||||||
|
const result = await project.renameProject(newName)
|
||||||
|
if (!result.ok) {
|
||||||
|
renameError.reportError(result.error)
|
||||||
|
}
|
||||||
|
projectNameEdited.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="NavBreadcrumbs">
|
<div class="NavBreadcrumbs">
|
||||||
<template v-for="(breadcrumb, index) in props.breadcrumbs" :key="index">
|
<SvgButton name="edit" title="Edit Project Name" @click.stop="projectNameEdited = true" />
|
||||||
|
<template v-for="(breadcrumb, index) in stackNavigator.breadcrumbLabels.value" :key="index">
|
||||||
<SvgButton
|
<SvgButton
|
||||||
v-if="index > 0"
|
v-if="index > 0"
|
||||||
name="arrow_right_head_only"
|
name="arrow_right_head_only"
|
||||||
:disabled="breadcrumb.active"
|
:disabled="!breadcrumb.active"
|
||||||
class="arrow"
|
class="arrow"
|
||||||
/>
|
/>
|
||||||
<NavBreadcrumb
|
<NavBreadcrumb
|
||||||
:text="breadcrumb.label"
|
:modelValue="breadcrumb.label"
|
||||||
:active="breadcrumb.active"
|
:active="breadcrumb.active"
|
||||||
|
:editing="index === 0 && projectNameEdited"
|
||||||
:title="index === 0 ? 'Project Name' : ''"
|
:title="index === 0 ? 'Project Name' : ''"
|
||||||
class="clickable"
|
class="clickable"
|
||||||
@click.stop="emit('selected', index)"
|
@click.stop="stackNavigator.handleBreadcrumbClick(index)"
|
||||||
|
@update:modelValue="renameBreadcrumb(index, $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ExtendedMenu from '@/components/ExtendedMenu.vue'
|
import ExtendedMenu from '@/components/ExtendedMenu.vue'
|
||||||
import NavBar from '@/components/NavBar.vue'
|
import NavBar from '@/components/NavBar.vue'
|
||||||
import type { BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
|
|
||||||
import RecordControl from '@/components/RecordControl.vue'
|
import RecordControl from '@/components/RecordControl.vue'
|
||||||
import SelectionMenu from '@/components/SelectionMenu.vue'
|
import SelectionMenu from '@/components/SelectionMenu.vue'
|
||||||
import { injectGuiConfig } from '@/providers/guiConfig'
|
import { injectGuiConfig } from '@/providers/guiConfig'
|
||||||
@ -11,18 +10,12 @@ const showColorPicker = defineModel<boolean>('showColorPicker', { required: true
|
|||||||
const showCodeEditor = defineModel<boolean>('showCodeEditor', { required: true })
|
const showCodeEditor = defineModel<boolean>('showCodeEditor', { required: true })
|
||||||
const showDocumentationEditor = defineModel<boolean>('showDocumentationEditor', { required: true })
|
const showDocumentationEditor = defineModel<boolean>('showDocumentationEditor', { required: true })
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
breadcrumbs: BreadcrumbItem[]
|
|
||||||
recordMode: boolean
|
recordMode: boolean
|
||||||
allowNavigationLeft: boolean
|
|
||||||
allowNavigationRight: boolean
|
|
||||||
zoomLevel: number
|
zoomLevel: number
|
||||||
componentsSelected: number
|
componentsSelected: number
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
recordOnce: []
|
recordOnce: []
|
||||||
back: []
|
|
||||||
forward: []
|
|
||||||
breadcrumbClick: [index: number]
|
|
||||||
'update:recordMode': [enabled: boolean]
|
'update:recordMode': [enabled: boolean]
|
||||||
fitToAllClicked: []
|
fitToAllClicked: []
|
||||||
zoomIn: []
|
zoomIn: []
|
||||||
@ -50,14 +43,7 @@ const barStyle = computed(() => {
|
|||||||
@update:recordMode="emit('update:recordMode', $event)"
|
@update:recordMode="emit('update:recordMode', $event)"
|
||||||
@recordOnce="emit('recordOnce')"
|
@recordOnce="emit('recordOnce')"
|
||||||
/>
|
/>
|
||||||
<NavBar
|
<NavBar />
|
||||||
:breadcrumbs="props.breadcrumbs"
|
|
||||||
:allowNavigationLeft="props.allowNavigationLeft"
|
|
||||||
:allowNavigationRight="props.allowNavigationRight"
|
|
||||||
@back="emit('back')"
|
|
||||||
@forward="emit('forward')"
|
|
||||||
@breadcrumbClick="emit('breadcrumbClick', $event)"
|
|
||||||
/>
|
|
||||||
<Transition name="selection-menu">
|
<Transition name="selection-menu">
|
||||||
<SelectionMenu
|
<SelectionMenu
|
||||||
v-if="componentsSelected > 1"
|
v-if="componentsSelected > 1"
|
||||||
|
5
app/gui2/src/providers/graphStackNavigator.ts
Normal file
5
app/gui2/src/providers/graphStackNavigator.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { useStackNavigator } from '@/composables/stackNavigator'
|
||||||
|
import { createContextStore } from '@/providers'
|
||||||
|
|
||||||
|
export { injectFn as injectStackNavigator, provideFn as provideStackNavigator }
|
||||||
|
const { provideFn, injectFn } = createContextStore('graph stack navigator', useStackNavigator)
|
@ -2,6 +2,12 @@ import { findIndexOpt } from '@/util/data/array'
|
|||||||
import { isSome, type Opt } from '@/util/data/opt'
|
import { isSome, type Opt } from '@/util/data/opt'
|
||||||
import { Err, Ok, type Result } from '@/util/data/result'
|
import { Err, Ok, type Result } from '@/util/data/result'
|
||||||
import { AsyncQueue, type AbortScope } from '@/util/net'
|
import { AsyncQueue, type AbortScope } from '@/util/net'
|
||||||
|
import {
|
||||||
|
qnReplaceProjectName,
|
||||||
|
tryIdentifier,
|
||||||
|
tryQualifiedName,
|
||||||
|
type Identifier,
|
||||||
|
} from '@/util/qualifiedName'
|
||||||
import * as array from 'lib0/array'
|
import * as array from 'lib0/array'
|
||||||
import * as object from 'lib0/object'
|
import * as object from 'lib0/object'
|
||||||
import { ObservableV2 } from 'lib0/observable'
|
import { ObservableV2 } from 'lib0/observable'
|
||||||
@ -55,7 +61,7 @@ type ExecutionContextState =
|
|||||||
visualizations: Map<Uuid, NodeVisualizationConfiguration>
|
visualizations: Map<Uuid, NodeVisualizationConfiguration>
|
||||||
stack: StackItem[]
|
stack: StackItem[]
|
||||||
environment?: ExecutionEnvironment
|
environment?: ExecutionEnvironment
|
||||||
} // | { status: 'broken'} TODO[ao] think about it
|
}
|
||||||
|
|
||||||
type EntryPoint = Omit<ExplicitCall, 'type'>
|
type EntryPoint = Omit<ExplicitCall, 'type'>
|
||||||
|
|
||||||
@ -74,6 +80,13 @@ type ExecutionContextNotification = {
|
|||||||
'visualizationsConfigured'(configs: Set<Uuid>): void
|
'visualizationsConfigured'(configs: Set<Uuid>): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SyncStatus {
|
||||||
|
NOT_SYNCED,
|
||||||
|
QUEUED,
|
||||||
|
SYNCING,
|
||||||
|
SYNCED,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execution Context
|
* Execution Context
|
||||||
*
|
*
|
||||||
@ -86,7 +99,7 @@ type ExecutionContextNotification = {
|
|||||||
export class ExecutionContext extends ObservableV2<ExecutionContextNotification> {
|
export class ExecutionContext extends ObservableV2<ExecutionContextNotification> {
|
||||||
readonly id: ContextId = random.uuidv4() as ContextId
|
readonly id: ContextId = random.uuidv4() as ContextId
|
||||||
private queue: AsyncQueue<ExecutionContextState>
|
private queue: AsyncQueue<ExecutionContextState>
|
||||||
private syncScheduled = false
|
private syncStatus = SyncStatus.NOT_SYNCED
|
||||||
private clearScheduled = false
|
private clearScheduled = false
|
||||||
private _desiredStack: StackItem[] = reactive([])
|
private _desiredStack: StackItem[] = reactive([])
|
||||||
private visualizationConfigs: Map<Uuid, NodeVisualizationConfiguration> = new Map()
|
private visualizationConfigs: Map<Uuid, NodeVisualizationConfiguration> = new Map()
|
||||||
@ -135,7 +148,7 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
|||||||
// Connection closed: the created execution context is no longer available
|
// Connection closed: the created execution context is no longer available
|
||||||
// There is no point in any scheduled action until resynchronization
|
// There is no point in any scheduled action until resynchronization
|
||||||
this.queue.clear()
|
this.queue.clear()
|
||||||
this.syncScheduled = false
|
this.syncStatus = SyncStatus.NOT_SYNCED
|
||||||
this.queue.pushTask(() => {
|
this.queue.pushTask(() => {
|
||||||
this.clearScheduled = false
|
this.clearScheduled = false
|
||||||
this.sync()
|
this.sync()
|
||||||
@ -143,6 +156,32 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
|||||||
})
|
})
|
||||||
this.clearScheduled = true
|
this.clearScheduled = true
|
||||||
})
|
})
|
||||||
|
this.lsRpc.on('refactoring/projectRenamed', ({ oldNormalizedName, newNormalizedName }) => {
|
||||||
|
const newIdent = tryIdentifier(newNormalizedName)
|
||||||
|
if (!newIdent.ok) {
|
||||||
|
console.error(
|
||||||
|
`Cannot update project name in execution stack: new name ${newNormalizedName} is not a valid identifier!`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ExecutionContext.replaceProjectNameInStack(
|
||||||
|
this._desiredStack,
|
||||||
|
oldNormalizedName,
|
||||||
|
newIdent.value,
|
||||||
|
)
|
||||||
|
if (this.syncStatus === SyncStatus.SYNCED) {
|
||||||
|
this.queue.pushTask((state) => {
|
||||||
|
if (state.status !== 'created') return Promise.resolve(state)
|
||||||
|
ExecutionContext.replaceProjectNameInStack(state.stack, oldNormalizedName, newIdent.value)
|
||||||
|
return Promise.resolve(state)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Engine updates project name in its execution context frames by itself. But if we are out
|
||||||
|
// of sync, we have no guarantee if the stack wasn't set with old name after project rename.
|
||||||
|
// It's safer to just re-sync the stack.
|
||||||
|
this.sync()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private pushItem(item: StackItem) {
|
private pushItem(item: StackItem) {
|
||||||
@ -150,6 +189,28 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
|||||||
this.sync()
|
this.sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static replaceProjectNameInStack(
|
||||||
|
stack: StackItem[],
|
||||||
|
oldName: string,
|
||||||
|
newName: Identifier,
|
||||||
|
) {
|
||||||
|
const updatedField = (value: string) => {
|
||||||
|
const qn = tryQualifiedName(value)
|
||||||
|
if (qn.ok) {
|
||||||
|
return qnReplaceProjectName(qn.value, oldName, newName)
|
||||||
|
} else {
|
||||||
|
console.warn(`Invalid qualified name in execution context stack: ${value}`)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const item of stack) {
|
||||||
|
if (item.type === 'ExplicitCall') {
|
||||||
|
item.methodPointer.module = updatedField(item.methodPointer.module)
|
||||||
|
item.methodPointer.definedOnType = updatedField(item.methodPointer.definedOnType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get desiredStack() {
|
get desiredStack() {
|
||||||
return this._desiredStack
|
return this._desiredStack
|
||||||
}
|
}
|
||||||
@ -229,8 +290,8 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sync() {
|
private sync() {
|
||||||
if (this.syncScheduled || this.abort.signal.aborted) return
|
if (this.syncStatus === SyncStatus.QUEUED || this.abort.signal.aborted) return
|
||||||
this.syncScheduled = true
|
this.syncStatus = SyncStatus.QUEUED
|
||||||
this.queue.pushTask(this.syncTask())
|
this.queue.pushTask(this.syncTask())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +309,7 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
|||||||
|
|
||||||
private syncTask() {
|
private syncTask() {
|
||||||
return async (state: ExecutionContextState) => {
|
return async (state: ExecutionContextState) => {
|
||||||
this.syncScheduled = false
|
this.syncStatus = SyncStatus.SYNCING
|
||||||
if (this.abort.signal.aborted) return state
|
if (this.abort.signal.aborted) return state
|
||||||
let newState = { ...state }
|
let newState = { ...state }
|
||||||
|
|
||||||
@ -399,6 +460,9 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
|||||||
this.emit('visualizationsConfigured', [
|
this.emit('visualizationsConfigured', [
|
||||||
new Set(state.status === 'created' ? state.visualizations.keys() : []),
|
new Set(state.status === 'created' ? state.visualizations.keys() : []),
|
||||||
])
|
])
|
||||||
|
if (this.syncStatus === SyncStatus.SYNCING) {
|
||||||
|
this.syncStatus = SyncStatus.SYNCED
|
||||||
|
}
|
||||||
return newState
|
return newState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
markRaw,
|
markRaw,
|
||||||
onScopeDispose,
|
onScopeDispose,
|
||||||
proxyRefs,
|
proxyRefs,
|
||||||
|
readonly,
|
||||||
ref,
|
ref,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
watch,
|
watch,
|
||||||
@ -99,7 +100,7 @@ export type ProjectStore = ReturnType<typeof useProjectStore>
|
|||||||
*/
|
*/
|
||||||
export const { provideFn: provideProjectStore, injectFn: useProjectStore } = createContextStore(
|
export const { provideFn: provideProjectStore, injectFn: useProjectStore } = createContextStore(
|
||||||
'project',
|
'project',
|
||||||
() => {
|
(renameProjectBackend: (newName: string) => void) => {
|
||||||
const abort = useAbortScope()
|
const abort = useAbortScope()
|
||||||
|
|
||||||
const observedFileName = ref<string>()
|
const observedFileName = ref<string>()
|
||||||
@ -108,9 +109,10 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
|
|||||||
const awareness = new Awareness(doc)
|
const awareness = new Awareness(doc)
|
||||||
|
|
||||||
const config = injectGuiConfig()
|
const config = injectGuiConfig()
|
||||||
const projectName = config.value.startup?.project
|
const projectNameFromCfg = config.value.startup?.project
|
||||||
if (projectName == null) throw new Error('Missing project name.')
|
if (projectNameFromCfg == null) throw new Error('Missing project name.')
|
||||||
const projectDisplayName = config.value.startup?.displayedProjectName ?? projectName
|
const projectName = ref(projectNameFromCfg)
|
||||||
|
const projectDisplayName = ref(config.value.startup?.displayedProjectName ?? projectName)
|
||||||
|
|
||||||
const clientId = random.uuidv4() as Uuid
|
const clientId = random.uuidv4() as Uuid
|
||||||
const lsUrls = resolveLsUrl(config.value)
|
const lsUrls = resolveLsUrl(config.value)
|
||||||
@ -126,7 +128,6 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
|
|||||||
rpcUrl.hostname === '[::1]' ||
|
rpcUrl.hostname === '[::1]' ||
|
||||||
rpcUrl.hostname === '0:0:0:0:0:0:0:1'
|
rpcUrl.hostname === '0:0:0:0:0:0:0:1'
|
||||||
|
|
||||||
const name = computed(() => config.value.startup?.project)
|
|
||||||
const namespace = computed(() => config.value.engine?.namespace)
|
const namespace = computed(() => config.value.engine?.namespace)
|
||||||
const fullName = computed(() => {
|
const fullName = computed(() => {
|
||||||
const ns = namespace.value
|
const ns = namespace.value
|
||||||
@ -135,14 +136,7 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
|
|||||||
'Unknown project\'s namespace. Assuming "local", however it likely won\'t work in cloud',
|
'Unknown project\'s namespace. Assuming "local", however it likely won\'t work in cloud',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const projectName = name.value
|
return `${ns ?? 'local'}.${projectName.value}`
|
||||||
if (projectName == null) {
|
|
||||||
console.error(
|
|
||||||
"Unknown project's name. Cannot specify opened module's qualified path; many things may not work",
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return `${ns ?? 'local'}.${projectName}`
|
|
||||||
})
|
})
|
||||||
const modulePath = computed(() => {
|
const modulePath = computed(() => {
|
||||||
const filePath = observedFileName.value
|
const filePath = observedFileName.value
|
||||||
@ -347,6 +341,24 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function renameProject(newDisplayedName: string) {
|
||||||
|
try {
|
||||||
|
renameProjectBackend(newDisplayedName)
|
||||||
|
return Ok()
|
||||||
|
} catch (err) {
|
||||||
|
return Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lsRpcConnection.on(
|
||||||
|
'refactoring/projectRenamed',
|
||||||
|
({ oldNormalizedName, newNormalizedName, newName }) => {
|
||||||
|
if (oldNormalizedName === projectName.value) {
|
||||||
|
projectName.value = newNormalizedName
|
||||||
|
projectDisplayName.value = newName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const projectRootId = contentRoots.then(
|
const projectRootId = contentRoots.then(
|
||||||
(roots) => roots.find((root) => root.type === 'Project')?.id,
|
(roots) => roots.find((root) => root.type === 'Project')?.id,
|
||||||
)
|
)
|
||||||
@ -370,8 +382,8 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
|
|||||||
get observedFileName() {
|
get observedFileName() {
|
||||||
return observedFileName.value
|
return observedFileName.value
|
||||||
},
|
},
|
||||||
name: projectName,
|
name: readonly(projectName),
|
||||||
displayName: projectDisplayName,
|
displayName: readonly(projectDisplayName),
|
||||||
isOnLocalBackend,
|
isOnLocalBackend,
|
||||||
executionContext,
|
executionContext,
|
||||||
firstExecution,
|
firstExecution,
|
||||||
@ -394,6 +406,7 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
|
|||||||
dataflowErrors,
|
dataflowErrors,
|
||||||
executeExpression,
|
executeExpression,
|
||||||
disposeYDocsProvider,
|
disposeYDocsProvider,
|
||||||
|
renameProject,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,9 @@ import {
|
|||||||
qnJoin,
|
qnJoin,
|
||||||
qnLastSegment,
|
qnLastSegment,
|
||||||
qnParent,
|
qnParent,
|
||||||
|
qnReplaceProjectName,
|
||||||
qnSplit,
|
qnSplit,
|
||||||
|
tryIdentifier,
|
||||||
tryIdentifierOrOperatorIdentifier,
|
tryIdentifierOrOperatorIdentifier,
|
||||||
tryQualifiedName,
|
tryQualifiedName,
|
||||||
type IdentifierOrOperatorIdentifier,
|
type IdentifierOrOperatorIdentifier,
|
||||||
@ -96,3 +98,15 @@ test.each([
|
|||||||
const qn = unwrap(tryQualifiedName(name))
|
const qn = unwrap(tryQualifiedName(name))
|
||||||
expect(normalizeQualifiedName(qn)).toEqual(unwrap(tryQualifiedName(expected)))
|
expect(normalizeQualifiedName(qn)).toEqual(unwrap(tryQualifiedName(expected)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['local.Project.Main', 'Project', 'NewProject', 'local.NewProject.Main'],
|
||||||
|
['local.Project.Main', 'Project2', 'NewProject', 'local.Project.Main'],
|
||||||
|
['local.Project', 'Project', 'NewProject', 'local.NewProject'],
|
||||||
|
['Project', 'Project', 'NewProject', 'Project'],
|
||||||
|
['local.Project2.Project', 'Project', 'NewProject', 'local.Project2.Project'],
|
||||||
|
])('Replacing project name in %s from %s to %s', (qname, oldName, newName, expected) => {
|
||||||
|
const qn = unwrap(tryQualifiedName(qname))
|
||||||
|
const newIdent = unwrap(tryIdentifier(newName))
|
||||||
|
expect(qnReplaceProjectName(qn, oldName, newIdent)).toBe(expected)
|
||||||
|
})
|
||||||
|
@ -97,3 +97,19 @@ export function qnSlice(
|
|||||||
export function qnIsTopElement(name: QualifiedName): boolean {
|
export function qnIsTopElement(name: QualifiedName): boolean {
|
||||||
return !/[.].*?[.].*?[.]/.test(name)
|
return !/[.].*?[.].*?[.]/.test(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the project name in this qualified name if equal to `oldProject`, otherwise return `qn`.
|
||||||
|
*
|
||||||
|
* The namespace will be unchanged.
|
||||||
|
*/
|
||||||
|
export function qnReplaceProjectName(
|
||||||
|
qn: QualifiedName,
|
||||||
|
oldProject: string,
|
||||||
|
newProject: Identifier,
|
||||||
|
): QualifiedName {
|
||||||
|
return qn.replace(
|
||||||
|
new RegExp(`^(${identifierRegexPart}\\.)${oldProject}(?=\\.|$)`),
|
||||||
|
`$1${newProject}`,
|
||||||
|
) as QualifiedName
|
||||||
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import GraphEditor from '@/components/GraphEditor.vue'
|
import GraphEditor from '@/components/GraphEditor.vue'
|
||||||
|
import { provideProjectStore } from '@/stores/project'
|
||||||
|
|
||||||
|
const props = defineProps<{ renameProject: (newName: string) => void }>()
|
||||||
|
|
||||||
|
provideProjectStore(props.renameProject)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -8,7 +8,10 @@ import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
|||||||
|
|
||||||
import * as backendProvider from '#/providers/BackendProvider'
|
import * as backendProvider from '#/providers/BackendProvider'
|
||||||
|
|
||||||
import type * as backendModule from '#/services/Backend'
|
import type Backend from '#/services/Backend'
|
||||||
|
import * as backendModule from '#/services/Backend'
|
||||||
|
|
||||||
|
import * as object from '#/utilities/object'
|
||||||
|
|
||||||
import type * as types from '../../../types/types'
|
import type * as types from '../../../types/types'
|
||||||
|
|
||||||
@ -32,6 +35,7 @@ export default function Editor(props: EditorProps) {
|
|||||||
const gtagEventRef = React.useRef(gtagEvent)
|
const gtagEventRef = React.useRef(gtagEvent)
|
||||||
gtagEventRef.current = gtagEvent
|
gtagEventRef.current = gtagEvent
|
||||||
const remoteBackend = backendProvider.useRemoteBackend()
|
const remoteBackend = backendProvider.useRemoteBackend()
|
||||||
|
const localBackend = backendProvider.useLocalBackend()
|
||||||
|
|
||||||
const logEvent = React.useCallback(
|
const logEvent = React.useCallback(
|
||||||
(message: string, projectId?: string | null, metadata?: object | null) => {
|
(message: string, projectId?: string | null, metadata?: object | null) => {
|
||||||
@ -42,6 +46,36 @@ export default function Editor(props: EditorProps) {
|
|||||||
[remoteBackend]
|
[remoteBackend]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renameProject = React.useCallback(
|
||||||
|
(newName: string) => {
|
||||||
|
if (projectStartupInfo != null) {
|
||||||
|
let backend: Backend | null
|
||||||
|
switch (projectStartupInfo.backendType) {
|
||||||
|
case backendModule.BackendType.local:
|
||||||
|
backend = localBackend
|
||||||
|
break
|
||||||
|
case backendModule.BackendType.remote:
|
||||||
|
backend = remoteBackend
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const { id: projectId, parentId, title } = projectStartupInfo.projectAsset
|
||||||
|
backend
|
||||||
|
?.updateProject(
|
||||||
|
projectId,
|
||||||
|
{ projectName: newName, ami: null, ideVersion: null, parentId },
|
||||||
|
title
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
() => {
|
||||||
|
projectStartupInfo.setProjectAsset?.(object.merger({ title: newName }))
|
||||||
|
},
|
||||||
|
e => toastAndLog('renameProjectError', e)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[remoteBackend, localBackend, projectStartupInfo, toastAndLog]
|
||||||
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return
|
return
|
||||||
@ -84,9 +118,10 @@ export default function Editor(props: EditorProps) {
|
|||||||
hidden,
|
hidden,
|
||||||
ignoreParamsRegex: new RegExp(`^${appUtils.SEARCH_PARAMS_PREFIX}(.+)$`),
|
ignoreParamsRegex: new RegExp(`^${appUtils.SEARCH_PARAMS_PREFIX}(.+)$`),
|
||||||
logEvent,
|
logEvent,
|
||||||
|
renameProject,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [projectStartupInfo, toastAndLog, hidden, logEvent, ydocUrl])
|
}, [projectStartupInfo, toastAndLog, hidden, logEvent, ydocUrl, renameProject])
|
||||||
|
|
||||||
if (projectStartupInfo == null || AppRunner == null || appProps == null) {
|
if (projectStartupInfo == null || AppRunner == null || appProps == null) {
|
||||||
return <></>
|
return <></>
|
||||||
|
1
app/ide-desktop/lib/types/types.d.ts
vendored
1
app/ide-desktop/lib/types/types.d.ts
vendored
@ -21,6 +21,7 @@ interface EditorProps {
|
|||||||
projectId?: string | null,
|
projectId?: string | null,
|
||||||
metadata?: object | null
|
metadata?: object | null
|
||||||
) => void
|
) => void
|
||||||
|
readonly renameProject: (newName: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The value passed from the entrypoint to the dashboard, which enables the dashboard to
|
/** The value passed from the entrypoint to the dashboard, which enables the dashboard to
|
||||||
|
Loading…
Reference in New Issue
Block a user