diff --git a/.node-version b/.node-version index 2dbbe00e679..7af24b7ddbd 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.11.1 +22.11.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index d45a8dea3a0..e9e229a6a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ - [Table Input Widget has now a limit of 256 cells.][11448] - [Added an error message screen displayed when viewing a deleted component.][11452] +- [New documentation editor provides improved Markdown editing experience, and + paves the way for new documentation features.][11469] [11151]: https://github.com/enso-org/enso/pull/11151 [11271]: https://github.com/enso-org/enso/pull/11271 @@ -37,6 +39,7 @@ [11447]: https://github.com/enso-org/enso/pull/11447 [11448]: https://github.com/enso-org/enso/pull/11448 [11452]: https://github.com/enso-org/enso/pull/11452 +[11469]: https://github.com/enso-org/enso/pull/11469 #### Enso Standard Library @@ -46,11 +49,14 @@ programmatically.][11255] - [DB_Table may be saved as a Data Link.][11371] - [Support for dates before 1900 in Excel and signed AWS requests.][11373] +- [Added `Data.read_many` that allows to read a list of files in a single + operation.][11490] [11235]: https://github.com/enso-org/enso/pull/11235 [11255]: https://github.com/enso-org/enso/pull/11255 [11371]: https://github.com/enso-org/enso/pull/11371 [11373]: https://github.com/enso-org/enso/pull/11373 +[11490]: https://github.com/enso-org/enso/pull/11490 #### Enso Language & Runtime diff --git a/app/common/package.json b/app/common/package.json index d3bb6c880ee..2d34ca4e4e7 100644 --- a/app/common/package.json +++ b/app/common/package.json @@ -18,6 +18,8 @@ "./src/utilities/data/dateTime": "./src/utilities/data/dateTime.ts", "./src/utilities/data/newtype": "./src/utilities/data/newtype.ts", "./src/utilities/data/object": "./src/utilities/data/object.ts", + "./src/utilities/data/string": "./src/utilities/data/string.ts", + "./src/utilities/data/iter": "./src/utilities/data/iter.ts", "./src/utilities/style/tabBar": "./src/utilities/style/tabBar.ts", "./src/utilities/uniqueString": "./src/utilities/uniqueString.ts", "./src/text": "./src/text/index.ts", @@ -37,6 +39,7 @@ "@tanstack/query-persist-client-core": "^5.54.0", "@tanstack/vue-query": ">= 5.54.0 < 5.56.0", "idb-keyval": "^6.2.1", + "lib0": "^0.2.85", "react": "^18.3.1", "vitest": "^1.3.1", "vue": "^3.5.2" diff --git a/app/common/src/text/index.ts b/app/common/src/text/index.ts index 3a84cfd829a..11d3f3f1429 100644 --- a/app/common/src/text/index.ts +++ b/app/common/src/text/index.ts @@ -1,6 +1,6 @@ /** @file Functions related to displaying text. */ -import ENGLISH from './english.json' assert { type: 'json' } +import ENGLISH from './english.json' with { type: 'json' } // ============= // === Types === diff --git a/app/common/src/utilities/data/__tests__/iterator.test.ts b/app/common/src/utilities/data/__tests__/iterator.test.ts new file mode 100644 index 00000000000..6bf76baff14 --- /dev/null +++ b/app/common/src/utilities/data/__tests__/iterator.test.ts @@ -0,0 +1,146 @@ +import { expect, test } from 'vitest' +import * as iter from '../iter' + +interface IteratorCase { + iterable: Iterable + soleValue: T | undefined + first: T | undefined + last: T | undefined + count: number +} + +function makeCases(): IteratorCase[] { + return [ + { + iterable: iter.empty(), + soleValue: undefined, + first: undefined, + last: undefined, + count: 0, + }, + { + iterable: iter.chain(iter.empty(), iter.empty()), + soleValue: undefined, + first: undefined, + last: undefined, + count: 0, + }, + { + iterable: iter.chain(iter.empty(), ['a'], iter.empty()), + soleValue: 'a', + first: 'a', + last: 'a', + count: 1, + }, + { + iterable: iter.range(10, 11), + soleValue: 10, + first: 10, + last: 10, + count: 1, + }, + { + iterable: iter.range(10, 20), + soleValue: undefined, + first: 10, + last: 19, + count: 10, + }, + { + iterable: iter.range(20, 10), + soleValue: undefined, + first: 20, + last: 11, + count: 10, + }, + { + iterable: [], + soleValue: undefined, + first: undefined, + last: undefined, + count: 0, + }, + { + iterable: ['a'], + soleValue: 'a', + first: 'a', + last: 'a', + count: 1, + }, + { + iterable: ['a', 'b'], + soleValue: undefined, + first: 'a', + last: 'b', + count: 2, + }, + { + iterable: iter.filterDefined([undefined, 'a', undefined, 'b', undefined]), + soleValue: undefined, + first: 'a', + last: 'b', + count: 2, + }, + { + iterable: iter.filter([7, 'a', 8, 'b', 9], el => typeof el === 'string'), + soleValue: undefined, + first: 'a', + last: 'b', + count: 2, + }, + { + iterable: iter.zip(['a', 'b'], iter.range(1, 2)), + soleValue: ['a', 1], + first: ['a', 1], + last: ['a', 1], + count: 1, + }, + { + iterable: iter.zip(['a', 'b'], iter.range(1, 3)), + soleValue: undefined, + first: ['a', 1], + last: ['b', 2], + count: 2, + }, + { + iterable: iter.zip(['a', 'b'], iter.range(1, 4)), + soleValue: undefined, + first: ['a', 1], + last: ['b', 2], + count: 2, + }, + { + iterable: iter.zipLongest(['a', 'b'], iter.range(1, 2)), + soleValue: undefined, + first: ['a', 1], + last: ['b', undefined], + count: 2, + }, + { + iterable: iter.zipLongest(['a', 'b'], iter.range(1, 3)), + soleValue: undefined, + first: ['a', 1], + last: ['b', 2], + count: 2, + }, + { + iterable: iter.zipLongest(['a', 'b'], iter.range(1, 4)), + soleValue: undefined, + first: ['a', 1], + last: [undefined, 3], + count: 3, + }, + ] +} + +test.each(makeCases())('tryGetSoleValue: case %#', ({ iterable, soleValue }) => { + expect(iter.tryGetSoleValue(iterable)).toEqual(soleValue) +}) + +test.each(makeCases())('last: case %#', ({ iterable, last }) => { + expect(iter.last(iterable)).toEqual(last) +}) + +test.each(makeCases())('count: case %#', ({ iterable, count }) => { + expect(iter.count(iterable)).toEqual(count) +}) diff --git a/app/ydoc-shared/src/util/data/iterable.ts b/app/common/src/utilities/data/iter.ts similarity index 60% rename from app/ydoc-shared/src/util/data/iterable.ts rename to app/common/src/utilities/data/iter.ts index eb1a911d81a..aaaa66742e0 100644 --- a/app/ydoc-shared/src/util/data/iterable.ts +++ b/app/common/src/utilities/data/iter.ts @@ -1,4 +1,30 @@ -/** @file Functions for manipulating {@link Iterable}s. */ +/** @file Utilities for manipulating {@link Iterator}s and {@link Iterable}s. */ + +import { iteratorFilter, mapIterator } from 'lib0/iterator' + +/** Similar to {@link Array.prototype.reduce|}, but consumes elements from any iterable. */ +export function reduce( + iterable: Iterable, + f: (accumulator: A, element: T) => A, + initialAccumulator: A, +): A { + const iterator = iterable[Symbol.iterator]() + let accumulator = initialAccumulator + let result = iterator.next() + while (!result.done) { + accumulator = f(accumulator, result.value) + result = iterator.next() + } + return accumulator +} + +/** + * Iterates the provided iterable, returning the number of elements it yielded. Note that if the input is an iterator, + * it will be consumed. + */ +export function count(it: Iterable): number { + return reduce(it, a => a + 1, 0) +} /** An iterable with zero elements. */ export function* empty(): Generator {} @@ -26,22 +52,17 @@ export function* range(start: number, stop: number, step = start <= stop ? 1 : - } } -/** - * Return an {@link Iterable} that `yield`s values that are the result of calling the given - * function on the next value of the given source iterable. - */ -export function* map(iter: Iterable, map: (value: T) => U): IterableIterator { - for (const value of iter) { - yield map(value) - } +/** @returns An iterator that yields the results of applying the given function to each value of the given iterable. */ +export function map(it: Iterable, f: (value: T) => U): IterableIterator { + return mapIterator(it[Symbol.iterator](), f) } /** * Return an {@link Iterable} that `yield`s only the values from the given source iterable * that pass the given predicate. */ -export function* filter(iter: Iterable, include: (value: T) => boolean): IterableIterator { - for (const value of iter) if (include(value)) yield value +export function filter(iter: Iterable, include: (value: T) => boolean): IterableIterator { + return iteratorFilter(iter[Symbol.iterator](), include) } /** @@ -141,3 +162,45 @@ export class Resumable { } } } + +/** Returns an iterator that yields the values of the provided iterator that are not strictly-equal to `undefined`. */ +export function* filterDefined(iterable: Iterable): IterableIterator { + for (const value of iterable) { + if (value !== undefined) yield value + } +} + +/** + * Returns whether the predicate returned `true` for all values yielded by the provided iterator. Short-circuiting. + * Returns `true` if the iterator doesn't yield any values. + */ +export function every(iter: Iterable, f: (value: T) => boolean): boolean { + for (const value of iter) if (!f(value)) return false + return true +} + +/** Return the first element returned by the iterable which meets the condition. */ +export function find(iter: Iterable, f: (value: T) => boolean): T | undefined { + for (const value of iter) { + if (f(value)) return value + } + return undefined +} + +/** Returns the first element yielded by the iterable. */ +export function first(iterable: Iterable): T | undefined { + const iterator = iterable[Symbol.iterator]() + const result = iterator.next() + return result.done ? undefined : result.value +} + +/** + * Return last element returned by the iterable. + * NOTE: Linear complexity. This function always visits the whole iterable. Using this with an + * infinite generator will cause an infinite loop. + */ +export function last(iter: Iterable): T | undefined { + let last + for (const el of iter) last = el + return last +} diff --git a/app/common/src/utilities/data/object.ts b/app/common/src/utilities/data/object.ts index f8010da8aef..8480594c23c 100644 --- a/app/common/src/utilities/data/object.ts +++ b/app/common/src/utilities/data/object.ts @@ -162,3 +162,24 @@ export type ExtractKeys = { /** An instance method of the given type. */ export type MethodOf = (this: T, ...args: never) => unknown + +// =================== +// === useObjectId === +// =================== + +/** Composable providing support for managing object identities. */ +export function useObjectId() { + let lastId = 0 + const idNumbers = new WeakMap() + /** @returns A value that can be used to compare object identity. */ + function objectId(o: object): number { + const id = idNumbers.get(o) + if (id == null) { + lastId += 1 + idNumbers.set(o, lastId) + return lastId + } + return id + } + return { objectId } +} diff --git a/app/common/src/utilities/data/string.ts b/app/common/src/utilities/data/string.ts new file mode 100644 index 00000000000..c2aeebbc7db --- /dev/null +++ b/app/common/src/utilities/data/string.ts @@ -0,0 +1,2 @@ +/** See http://www.unicode.org/reports/tr18/#Line_Boundaries */ +export const LINE_BOUNDARIES = /\r\n|[\n\v\f\r\x85\u2028\u2029]/g diff --git a/app/gui/e2e/project-view/locate.ts b/app/gui/e2e/project-view/locate.ts index f4867422c64..134a65144bf 100644 --- a/app/gui/e2e/project-view/locate.ts +++ b/app/gui/e2e/project-view/locate.ts @@ -84,7 +84,7 @@ export const addNewNodeButton = componentLocator('.PlusButton') export const componentBrowser = componentLocator('.ComponentBrowser') export const nodeOutputPort = componentLocator('.outputPortHoverArea') export const smallPlusButton = componentLocator('.SmallPlusButton') -export const lexicalContent = componentLocator('.LexicalContent') +export const editorRoot = componentLocator('.EditorRoot') /** * A not-selected variant of Component Browser Entry. diff --git a/app/gui/e2e/project-view/rightPanel.spec.ts b/app/gui/e2e/project-view/rightPanel.spec.ts index 474231e5eca..bc907fbe2d6 100644 --- a/app/gui/e2e/project-view/rightPanel.spec.ts +++ b/app/gui/e2e/project-view/rightPanel.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from 'playwright/test' import * as actions from './actions' -import { mockMethodCallInfo } from './expressionUpdates' +import { mockCollapsedFunctionInfo, mockMethodCallInfo } from './expressionUpdates' import { CONTROL_KEY } from './keyboard' import * as locate from './locate' @@ -13,7 +13,7 @@ test('Main method documentation', async ({ page }) => { await expect(locate.rightDock(page)).toBeVisible() // Right-dock displays main method documentation. - await expect(locate.lexicalContent(locate.rightDock(page))).toHaveText('The main method') + await expect(locate.editorRoot(locate.rightDock(page))).toHaveText('The main method') // Documentation hotkey closes right-dock.p await page.keyboard.press(`${CONTROL_KEY}+D`) @@ -70,3 +70,20 @@ test('Component help', async ({ page }) => { await locate.graphNodeByBinding(page, 'data').click() await expect(locate.rightDock(page)).toHaveText(/Reads a file into Enso/) }) + +test('Documentation reflects entered function', async ({ page }) => { + await actions.goToGraph(page) + + // Open the panel + await expect(locate.rightDock(page)).toBeHidden() + await page.keyboard.press(`${CONTROL_KEY}+D`) + await expect(locate.rightDock(page)).toBeVisible() + + // Enter the collapsed function + await mockCollapsedFunctionInfo(page, 'final', 'func1') + await locate.graphNodeByBinding(page, 'final').dblclick() + await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project', 'func1']) + + // Editor should contain collapsed function's docs + await expect(locate.editorRoot(locate.rightDock(page))).toHaveText('A collapsed function') +}) diff --git a/app/gui/e2e/project-view/setup.ts b/app/gui/e2e/project-view/setup.ts index 99ccf57dee5..bb9daa210ad 100644 --- a/app/gui/e2e/project-view/setup.ts +++ b/app/gui/e2e/project-view/setup.ts @@ -1,6 +1,6 @@ import { Server } from '@open-rpc/server-js' import * as random from 'lib0/random' -import pmSpec from './pm-openrpc.json' assert { type: 'json' } +import pmSpec from './pm-openrpc.json' with { type: 'json' } import { methods as pmMethods, projects, diff --git a/app/gui/package.json b/app/gui/package.json index 2d386f285f0..dd70b90b337 100644 --- a/app/gui/package.json +++ b/app/gui/package.json @@ -83,22 +83,18 @@ "babel-plugin-react-compiler": "19.0.0-beta-9ee70a1-20241017", "@codemirror/commands": "^6.6.0", "@codemirror/language": "^6.10.2", + "@codemirror/lang-markdown": "^v6.3.0", "@codemirror/lint": "^6.8.1", "@codemirror/search": "^6.5.6", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.28.3", "@fast-check/vitest": "^0.0.8", "@floating-ui/vue": "^1.0.6", - "@lexical/code": "^0.16.0", "@lexical/link": "^0.16.0", - "@lexical/list": "^0.16.0", - "@lexical/markdown": "^0.16.0", "@lexical/plain-text": "^0.16.0", - "@lexical/rich-text": "^0.16.0", - "@lexical/selection": "^0.16.0", - "@lexical/table": "^0.16.0", "@lexical/utils": "^0.16.0", "@lezer/common": "^1.1.0", + "@lezer/markdown": "^1.3.1", "@lezer/highlight": "^1.1.6", "@noble/hashes": "^1.4.0", "@vueuse/core": "^10.4.1", @@ -135,14 +131,13 @@ "@playwright/test": "^1.40.0", "@react-types/shared": "^3.22.1", "@tanstack/react-query-devtools": "5.45.1", - "@types/node": "^20.11.21", + "@types/node": "^22.9.0", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "@types/validator": "^13.11.7", "@vitejs/plugin-react": "^4.3.3", "chalk": "^5.3.0", "cross-env": "^7.0.3", - "enso-chat": "git://github.com/enso-org/enso-bot", "fast-check": "^3.15.0", "playwright": "^1.39.0", "postcss": "^8.4.29", @@ -189,7 +184,7 @@ "sql-formatter": "^13.0.0", "tar": "^6.2.1", "tsx": "^4.7.1", - "vite-plugin-vue-devtools": "7.3.7", + "vite-plugin-vue-devtools": "7.6.3", "vite-plugin-wasm": "^3.3.0", "vue-react-wrapper": "^0.3.1", "vue-tsc": "^2.0.24", diff --git a/app/gui/project-manager-shim-middleware/index.ts b/app/gui/project-manager-shim-middleware/index.ts index 609f5ff09b8..5d1c874fc5f 100644 --- a/app/gui/project-manager-shim-middleware/index.ts +++ b/app/gui/project-manager-shim-middleware/index.ts @@ -11,7 +11,7 @@ import * as tar from 'tar' import * as yaml from 'yaml' import * as common from 'enso-common' -import GLOBAL_CONFIG from 'enso-common/src/config.json' assert { type: 'json' } +import GLOBAL_CONFIG from 'enso-common/src/config.json' with { type: 'json' } import * as projectManagement from './projectManagement' diff --git a/app/gui/src/dashboard/layouts/Chat.tsx b/app/gui/src/dashboard/layouts/Chat.tsx index d41460d3594..b5834c7450b 100644 --- a/app/gui/src/dashboard/layouts/Chat.tsx +++ b/app/gui/src/dashboard/layouts/Chat.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import * as reactDom from 'react-dom' -import * as chat from 'enso-chat/chat' +import * as chat from '#/services/Chat' import CloseLargeIcon from '#/assets/close_large.svg' import DefaultUserIcon from '#/assets/default_user.svg' @@ -480,6 +480,7 @@ export default function Chat(props: ChatProps) { element.scrollTop = element.scrollHeight - element.clientHeight } // Auto-scroll MUST only happen when the message list changes. + // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps }, [messages]) diff --git a/app/gui/src/dashboard/services/Chat.ts b/app/gui/src/dashboard/services/Chat.ts new file mode 100644 index 00000000000..308cdf864e1 --- /dev/null +++ b/app/gui/src/dashboard/services/Chat.ts @@ -0,0 +1,260 @@ +/** + * @file An API definition for the chat WebSocket server. + * Types copied from the enso-bot server implementation: + * https://github.com/enso-org/enso-bot/blob/aa903b6e639a31930ee4fff55c5639e4471fa48d/chat.ts + */ + +import type * as newtype from '#/utilities/newtype' + +// ===================== +// === Message Types === +// ===================== + +/** Identifier for a chat Thread. */ +export type ThreadId = newtype.Newtype +/** Identifier for a chat message. */ +export type MessageId = newtype.Newtype +/** Identifier for a chat user. */ +export type UserId = newtype.Newtype +/** Chat user's email addresss. */ +export type EmailAddress = newtype.Newtype + +/** Enumeration of all message types exchanged with the chat server. */ +export enum ChatMessageDataType { + // Messages internal to the server. + /** Like the `authenticate` message, but with user details. */ + internalAuthenticate = 'internal-authenticate', + /** Like the `authenticateAnonymously` message, but with user details. */ + internalAuthenticateAnonymously = 'internal-authenticate-anonymously', + // Messages from the server to the client. + /** Metadata for all threads associated with a user. */ + serverThreads = 'server-threads', + /** Metadata for the currently open thread. */ + serverThread = 'server-thread', + /** A message from the server to the client. */ + serverMessage = 'server-message', + /** An edited message from the server to the client. */ + serverEditedMessage = 'server-edited-message', + /** + * A message from the client to the server, sent from the server to the client as part of + * the message history. + */ + serverReplayedMessage = 'server-replayed-message', + // Messages from the client to the server. + /** The authentication token. */ + authenticate = 'authenticate', + /** Sent by a user that is not logged in. This is currently only used on the website. */ + authenticateAnonymously = 'authenticate-anonymously', + /** Sent when the user is requesting scrollback history. */ + historyBefore = 'history-before', + /** Create a new thread with an initial message. */ + newThread = 'new-thread', + /** Rename an existing thread. */ + renameThread = 'rename-thread', + /** Change the currently active thread. */ + switchThread = 'switch-thread', + /** A message from the client to the server. */ + message = 'message', + /** A reaction from the client. */ + reaction = 'reaction', + /** Removal of a reaction from the client. */ + removeReaction = 'remove-reaction', + /** + * Mark a message as read. Used to determine whether to show the notification dot + * next to a thread. + */ + markAsRead = 'mark-as-read', +} + +/** Properties common to all WebSocket messages. */ +interface ChatBaseMessageData { + readonly type: Type +} + +// ========================= +// === Internal messages === +// ========================= + +/** Sent to the main file with user information. */ +export interface ChatInternalAuthenticateMessageData + extends ChatBaseMessageData { + readonly userId: UserId + readonly userName: string +} + +/** Sent to the main file with user IP. */ +export interface ChatInternalAuthenticateAnonymouslyMessageData + extends ChatBaseMessageData { + readonly userId: UserId + readonly email: EmailAddress +} + +// ====================================== +// === Messages from server to client === +// ====================================== + +/** All possible emojis that can be used as a reaction on a chat message. */ +export type ReactionSymbol = '❀️' | 'πŸŽ‰' | 'πŸ‘€' | 'πŸ‘' | 'πŸ‘Ž' | 'πŸ˜€' | 'πŸ™' + +/** Basic metadata for a single thread. */ +export interface ThreadData { + readonly title: string + readonly id: ThreadId + readonly hasUnreadMessages: boolean +} + +/** Basic metadata for a all of a user's threads. */ +export interface ChatServerThreadsMessageData + extends ChatBaseMessageData { + readonly threads: ThreadData[] +} + +/** All possible message types that may trigger a {@link ChatServerThreadMessageData} response. */ +export type ChatServerThreadRequestType = + | ChatMessageDataType.authenticate + | ChatMessageDataType.historyBefore + | ChatMessageDataType.newThread + | ChatMessageDataType.switchThread + +/** + * Thread details and recent messages. + * This message is sent every time the user switches threads. + */ +export interface ChatServerThreadMessageData + extends ChatBaseMessageData { + /** The type of the message that triggered this response. */ + readonly requestType: ChatServerThreadRequestType + readonly title: string + readonly id: ThreadId + /** `true` if there is no more message history before these messages. */ + readonly isAtBeginning: boolean + readonly messages: (ChatServerMessageMessageData | ChatServerReplayedMessageMessageData)[] +} + +/** A regular chat message from the server to the client. */ +export interface ChatServerMessageMessageData + extends ChatBaseMessageData { + readonly id: MessageId + // This should not be `null` for staff, as registration is required. + // However, it will be `null` for users that have not yet set an avatar. + readonly authorAvatar: string | null + readonly authorName: string + readonly content: string + readonly reactions: ReactionSymbol[] + /** Milliseconds since the Unix epoch. */ + readonly timestamp: number + /** + * Milliseconds since the Unix epoch. + * Should only be present when receiving message history, because new messages cannot have been + * edited. + */ + readonly editedTimestamp: number | null +} + +/** A regular edited chat message from the server to the client. */ +export interface ChatServerEditedMessageMessageData + extends ChatBaseMessageData { + readonly id: MessageId + readonly content: string + /** Milliseconds since the Unix epoch. */ + readonly timestamp: number +} + +/** A replayed message from the client to the server. Includes the timestamp of the message. */ +export interface ChatServerReplayedMessageMessageData + extends ChatBaseMessageData { + readonly id: MessageId + readonly content: string + /** Milliseconds since the Unix epoch. */ + readonly timestamp: number +} + +/** A message from the server to the client. */ +export type ChatServerMessageData = + | ChatServerEditedMessageMessageData + | ChatServerMessageMessageData + | ChatServerReplayedMessageMessageData + | ChatServerThreadMessageData + | ChatServerThreadsMessageData + +// ====================================== +// === Messages from client to server === +// ====================================== + +/** Sent whenever the user opens the chat sidebar. */ +export interface ChatAuthenticateMessageData + extends ChatBaseMessageData { + readonly accessToken: string +} + +/** Sent whenever the user opens the chat sidebar. */ +export interface ChatAuthenticateAnonymouslyMessageData + extends ChatBaseMessageData { + readonly email: EmailAddress +} + +/** Sent when the user is requesting scrollback history. */ +export interface ChatHistoryBeforeMessageData + extends ChatBaseMessageData { + readonly messageId: MessageId +} + +/** Sent when the user sends a message in a new thread. */ +export interface ChatNewThreadMessageData + extends ChatBaseMessageData { + readonly title: string + /** Content of the first message, to reduce the number of round trips. */ + readonly content: string +} + +/** Sent when the user finishes editing the thread name in the chat title bar. */ +export interface ChatRenameThreadMessageData + extends ChatBaseMessageData { + readonly title: string + readonly threadId: ThreadId +} + +/** Sent when the user picks a thread from the dropdown. */ +export interface ChatSwitchThreadMessageData + extends ChatBaseMessageData { + readonly threadId: ThreadId +} + +/** A regular message from the client to the server. */ +export interface ChatMessageMessageData extends ChatBaseMessageData { + readonly threadId: ThreadId + readonly content: string +} + +/** A reaction to a message sent by staff. */ +export interface ChatReactionMessageData extends ChatBaseMessageData { + readonly messageId: MessageId + readonly reaction: ReactionSymbol +} + +/** Removal of a reaction from the client. */ +export interface ChatRemoveReactionMessageData + extends ChatBaseMessageData { + readonly messageId: MessageId + readonly reaction: ReactionSymbol +} + +/** Sent when the user scrolls to the bottom of a chat thread. */ +export interface ChatMarkAsReadMessageData + extends ChatBaseMessageData { + readonly threadId: ThreadId + readonly messageId: MessageId +} + +/** A message from the client to the server. */ +export type ChatClientMessageData = + | ChatAuthenticateAnonymouslyMessageData + | ChatAuthenticateMessageData + | ChatHistoryBeforeMessageData + | ChatMarkAsReadMessageData + | ChatMessageMessageData + | ChatNewThreadMessageData + | ChatReactionMessageData + | ChatRemoveReactionMessageData + | ChatRenameThreadMessageData + | ChatSwitchThreadMessageData diff --git a/app/gui/src/project-view/assets/base.css b/app/gui/src/project-view/assets/base.css index 14f89717c21..0f8cdc1e5bd 100644 --- a/app/gui/src/project-view/assets/base.css +++ b/app/gui/src/project-view/assets/base.css @@ -85,7 +85,7 @@ /* Resize handle override for the visualization container. */ --visualization-resize-handle-inside: 3px; --visualization-resize-handle-outside: 3px; - --right-dock-default-width: 40%; + --right-dock-default-width: 40vw; --code-editor-default-height: 30%; --scrollbar-scrollable-opacity: 100%; } diff --git a/app/gui/src/project-view/components/CodeEditor.vue b/app/gui/src/project-view/components/CodeEditor.vue index 89577636dba..65c2286e980 100644 --- a/app/gui/src/project-view/components/CodeEditor.vue +++ b/app/gui/src/project-view/components/CodeEditor.vue @@ -1,14 +1,16 @@ diff --git a/app/gui/src/project-view/components/GraphEditor.vue b/app/gui/src/project-view/components/GraphEditor.vue index 714a27467bf..9e76ecec4b2 100644 --- a/app/gui/src/project-view/components/GraphEditor.vue +++ b/app/gui/src/project-view/components/GraphEditor.vue @@ -9,7 +9,7 @@ import { import BottomPanel from '@/components/BottomPanel.vue' import CodeEditor from '@/components/CodeEditor.vue' import ComponentBrowser from '@/components/ComponentBrowser.vue' -import { type Usage } from '@/components/ComponentBrowser/input' +import type { Usage } from '@/components/ComponentBrowser/input' import { usePlacement } from '@/components/ComponentBrowser/placement' import ComponentDocumentation from '@/components/ComponentDocumentation.vue' import DockPanel from '@/components/DockPanel.vue' @@ -20,21 +20,21 @@ import { useGraphEditorClipboard } from '@/components/GraphEditor/clipboard' import { performCollapse, prepareCollapsedInfo } from '@/components/GraphEditor/collapsing' import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation' import { useGraphEditorToasts } from '@/components/GraphEditor/toasts' -import { Uploader, uploadedExpression } from '@/components/GraphEditor/upload' +import { uploadedExpression, Uploader } from '@/components/GraphEditor/upload' import GraphMissingView from '@/components/GraphMissingView.vue' import GraphMouse from '@/components/GraphMouse.vue' import PlusButton from '@/components/PlusButton.vue' import SceneScroller from '@/components/SceneScroller.vue' import TopBar from '@/components/TopBar.vue' import { builtinWidgets } from '@/components/widgets' -import { useAstDocumentation } from '@/composables/astDocumentation' import { useDoubleClick } from '@/composables/doubleClick' import { keyboardBusy, keyboardBusyExceptIn, unrefElement, useEvent } from '@/composables/events' import { groupColorVar } from '@/composables/nodeColors' import type { PlacementStrategy } from '@/composables/nodeCreation' import { useSyncLocalStorage } from '@/composables/syncLocalStorage' import { provideFullscreenContext } from '@/providers/fullscreenContext' -import { provideGraphNavigator, type GraphNavigator } from '@/providers/graphNavigator' +import type { GraphNavigator } from '@/providers/graphNavigator' +import { provideGraphNavigator } from '@/providers/graphNavigator' import { provideNodeColors } from '@/providers/graphNodeColors' import { provideNodeCreation } from '@/providers/graphNodeCreation' import { provideGraphSelection } from '@/providers/graphSelection' @@ -43,25 +43,25 @@ import { provideInteractionHandler } from '@/providers/interactionHandler' import { provideKeyboard } from '@/providers/keyboard' import { injectVisibility } from '@/providers/visibility' import { provideWidgetRegistry } from '@/providers/widgetRegistry' -import { provideGraphStore, type NodeId } from '@/stores/graph' +import type { NodeId } from '@/stores/graph' +import { provideGraphStore } from '@/stores/graph' import type { RequiredImport } from '@/stores/graph/imports' import { useProjectStore } from '@/stores/project' import { useSettings } from '@/stores/settings' import { provideSuggestionDbStore } from '@/stores/suggestionDatabase' -import type { SuggestionId } from '@/stores/suggestionDatabase/entry' -import { suggestionDocumentationUrl, type Typename } from '@/stores/suggestionDatabase/entry' +import type { SuggestionId, Typename } from '@/stores/suggestionDatabase/entry' +import { suggestionDocumentationUrl } from '@/stores/suggestionDatabase/entry' import { provideVisualizationStore } from '@/stores/visualization' import { bail } from '@/util/assert' import { Ast } from '@/util/ast' -import type { AstId } from '@/util/ast/abstract' import { colorFromString } from '@/util/colors' import { partition } from '@/util/data/array' -import { every, filterDefined } from '@/util/data/iterable' import { Rect } from '@/util/data/rect' -import { Err, Ok, unwrapOr } from '@/util/data/result' +import { Err, Ok } from '@/util/data/result' import { Vec2 } from '@/util/data/vec2' import { computedFallback, useSelectRef } from '@/util/reactivity' import { until } from '@vueuse/core' +import * as iter from 'enso-common/src/utilities/data/iter' import { encoding, set } from 'lib0' import { computed, @@ -75,7 +75,6 @@ import { type ComponentInstance, } from 'vue' import { encodeMethodPointer } from 'ydoc-shared/languageServerTypes' -import * as iterable from 'ydoc-shared/util/data/iterable' import { isDevMode } from 'ydoc-shared/util/detect' const rootNode = ref() @@ -330,7 +329,7 @@ const graphBindingsHandler = graphBindings.handler({ }, toggleVisualization() { const selected = nodeSelection.selected - const allVisible = every( + const allVisible = iter.every( selected, (id) => graphStore.db.nodeIdToNode.get(id)?.vis?.visible === true, ) @@ -416,7 +415,7 @@ const documentationEditorArea = computed(() => unrefElement(docEditor)) const showRightDock = computedFallback( storedShowRightDock, // Show documentation editor when documentation exists on first graph visit. - () => !!documentation.state.value, + () => (markdownDocs.value?.length ?? 0) > 0, ) const rightDockTab = computedFallback(storedRightDockTab, () => 'docs') @@ -430,9 +429,11 @@ const documentationEditorHandler = documentationEditorBindings.handler({ }, }) -const { documentation } = useAstDocumentation(graphStore, () => - unwrapOr(graphStore.methodAst, undefined), -) +const markdownDocs = computed(() => { + const currentMethod = graphStore.methodAst + if (!currentMethod.ok) return + return currentMethod.value.mutableDocumentationMarkdown() +}) // === Component Browser === @@ -550,7 +551,7 @@ const componentBrowserElements = computed(() => [ interface NewNodeOptions { placement: PlacementStrategy - sourcePort?: AstId | undefined + sourcePort?: Ast.AstId | undefined } function addNodeDisconnected() { @@ -592,7 +593,7 @@ function createNodesFromSource(sourceNode: NodeId, options: NodeCreationOptions[ createWithComponentBrowser({ placement: { type: 'source', node: sourceNode }, sourcePort }) } -function handleNodeOutputPortDoubleClick(id: AstId) { +function handleNodeOutputPortDoubleClick(id: Ast.AstId) { const srcNode = graphStore.db.getPatternExpressionNodeId(id) if (srcNode == null) { console.error('Impossible happened: Double click on port not belonging to any node: ', id) @@ -601,7 +602,7 @@ function handleNodeOutputPortDoubleClick(id: AstId) { createWithComponentBrowser({ placement: { type: 'source', node: srcNode }, sourcePort: id }) } -function handleEdgeDrop(source: AstId, position: Vec2) { +function handleEdgeDrop(source: Ast.AstId, position: Vec2) { createWithComponentBrowser({ placement: { type: 'fixed', position }, sourcePort: source }) } @@ -609,7 +610,7 @@ function handleEdgeDrop(source: AstId, position: Vec2) { function collapseNodes() { const selected = new Set( - iterable.filter( + iter.filter( nodeSelection.selected, (id) => graphStore.db.nodeIdToNode.get(id)?.type === 'component', ), @@ -630,7 +631,7 @@ function collapseNodes() { if (!topLevel) { bail('BUG: no top level, collapsing not possible.') } - const selectedNodeRects = filterDefined(Array.from(selected, graphStore.visibleArea)) + const selectedNodeRects = iter.filterDefined(iter.map(selected, graphStore.visibleArea)) graphStore.edit((edit) => { const { collapsedCallRoot, collapsedNodeIds, outputAstId } = performCollapse( info.value, @@ -641,8 +642,8 @@ function collapseNodes() { const position = collapsedNodePlacement(selectedNodeRects) edit.get(collapsedCallRoot).mutableNodeMetadata().set('position', position.xy()) if (outputAstId != null) { - const collapsedNodeRects = filterDefined( - Array.from(collapsedNodeIds, graphStore.visibleArea), + const collapsedNodeRects = iter.filterDefined( + iter.map(collapsedNodeIds, graphStore.visibleArea), ) const { place } = usePlacement(collapsedNodeRects, graphNavigator.viewport) const position = place(collapsedNodeRects) @@ -785,9 +786,9 @@ const documentationEditorFullscreen = ref(false) > @@ -812,7 +813,7 @@ const documentationEditorFullscreen = ref(false) display: flex; flex-direction: row; - & :deep(.DockPanel) { + & .DockPanel { flex: none; } & .vertical { @@ -824,7 +825,7 @@ const documentationEditorFullscreen = ref(false) .vertical { display: flex; flex-direction: column; - & :deep(.BottomPanel) { + & .BottomPanel { flex: none; } & .viewport { diff --git a/app/gui/src/project-view/components/GraphEditor/GraphNode.vue b/app/gui/src/project-view/components/GraphEditor/GraphNode.vue index 56894d37ab0..006f4538b21 100644 --- a/app/gui/src/project-view/components/GraphEditor/GraphNode.vue +++ b/app/gui/src/project-view/components/GraphEditor/GraphNode.vue @@ -405,6 +405,10 @@ watchEffect(() => { emit('update:rect', nodeOuterRect.value) } }) + +const dataSource = computed( + () => ({ type: 'node', nodeId: props.node.rootExpr.externalId }) as const, +)