diff --git a/app/common/src/accessToken.ts b/app/common/src/accessToken.ts index 2a3c689994ca..64d650c28347 100644 --- a/app/common/src/accessToken.ts +++ b/app/common/src/accessToken.ts @@ -14,7 +14,9 @@ export interface AccessToken { readonly refreshToken: string /** The Cognito url to refresh the token. */ readonly refreshUrl: string - /** The when the token will expire. - * This is a string representation of a date in ISO 8601 format (e.g. "2021-01-01T00:00:00Z"). */ + /** + * The when the token will expire. + * This is a string representation of a date in ISO 8601 format (e.g. "2021-01-01T00:00:00Z"). + */ readonly expireAt: string } diff --git a/app/common/src/appConfig.d.ts b/app/common/src/appConfig.d.ts index 0a8bd8e07bc7..3411f03f67fc 100644 --- a/app/common/src/appConfig.d.ts +++ b/app/common/src/appConfig.d.ts @@ -1,17 +1,21 @@ /** @file Functions for managing app configuration. */ -/** Read environment variables from a file based on the `ENSO_CLOUD_ENV_FILE_NAME` +/** + * Read environment variables from a file based on the `ENSO_CLOUD_ENV_FILE_NAME` * environment variable. Reads from `.env` if the variable is blank or absent. - * DOES NOT override existing environment variables if the variable is absent. */ + * DOES NOT override existing environment variables if the variable is absent. + */ export function readEnvironmentFromFile(): Promise -/** An object containing app configuration to inject. +/** + * An object containing app configuration to inject. * * This includes: * - the base URL for backend endpoints * - the WebSocket URL for the chatbot * - the unique identifier for the cloud environment, for use in Sentry logs - * - Stripe, Sentry and Amplify public keys */ + * - Stripe, Sentry and Amplify public keys + */ export function getDefines(serverPort?: number): Record /** Load test environment variables, useful for when the Cloud backend is mocked or unnecessary. */ diff --git a/app/common/src/appConfig.js b/app/common/src/appConfig.js index 343f93305ce3..ccde478f1886 100644 --- a/app/common/src/appConfig.js +++ b/app/common/src/appConfig.js @@ -1,6 +1,7 @@ /** @file Functions for managing app configuration. */ import * as fs from 'node:fs/promises' import * as path from 'node:path' +import * as process from 'node:process' import * as url from 'node:url' // =============================== @@ -19,14 +20,13 @@ export async function readEnvironmentFromFile() { const filePath = path.join(url.fileURLToPath(new URL('../..', import.meta.url)), fileName) const buildInfo = await (async () => { try { - return await import('../../../../build.json', { with: { type: 'json' } }) + return await import('../../../build.json', { with: { type: 'json' } }) } catch { return { commit: '', version: '', engineVersion: '', name: '' } } })() try { const file = await fs.readFile(filePath, { encoding: 'utf-8' }) - // eslint-disable-next-line jsdoc/valid-types /** @type {readonly (readonly [string, string])[]} */ let entries = file.split('\n').flatMap(line => { if (/^\s*$|^.s*#/.test(line)) { @@ -47,14 +47,10 @@ export async function readEnvironmentFromFile() { if (!isProduction || entries.length > 0) { Object.assign(process.env, variables) } - // @ts-expect-error This is the only file where `process.env` should be written to. process.env.ENSO_CLOUD_DASHBOARD_VERSION ??= buildInfo.version - // @ts-expect-error This is the only file where `process.env` should be written to. process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH ??= buildInfo.commit } catch (error) { - // @ts-expect-error This is the only file where `process.env` should be written to. process.env.ENSO_CLOUD_DASHBOARD_VERSION ??= buildInfo.version - // @ts-expect-error This is the only file where `process.env` should be written to. process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH ??= buildInfo.commit const expectedKeys = Object.keys(DUMMY_DEFINES) .map(key => key.replace(/^process[.]env[.]/, '')) @@ -147,7 +143,6 @@ const DUMMY_DEFINES = { /** Load test environment variables, useful for when the Cloud backend is mocked or unnecessary. */ export function loadTestEnvironmentVariables() { for (const [k, v] of Object.entries(DUMMY_DEFINES)) { - // @ts-expect-error This is the only file where `process.env` should be written to. process.env[k.replace(/^process[.]env[.]/, '')] = v } } diff --git a/app/common/src/buildUtils.d.ts b/app/common/src/buildUtils.d.ts index 0223a7eb41ae..8bf7f411d45f 100644 --- a/app/common/src/buildUtils.d.ts +++ b/app/common/src/buildUtils.d.ts @@ -3,26 +3,34 @@ /** Indent size for stringifying JSON. */ export const INDENT_SIZE: number -/** Get the environment variable value. +/** + * Get the environment variable value. * @param name - The name of the environment variable. * @returns The value of the environment variable. - * @throws {Error} If the environment variable is not set. */ + * @throws {Error} If the environment variable is not set. + */ export function requireEnv(name: string): string -/** Read the path from environment variable and resolve it. +/** + * Read the path from environment variable and resolve it. * @param name - The name of the environment variable. * @returns The resolved path. - * @throws {Error} If the environment variable is not set. */ + * @throws {Error} If the environment variable is not set. + */ export function requireEnvResolvedPath(name: string): string -/** Read the path from environment variable and resolve it. Verify that it exists. +/** + * Read the path from environment variable and resolve it. Verify that it exists. * @param name - The name of the environment variable. * @returns The resolved path. - * @throws {Error} If the environment variable is not set or path does not exist. */ + * @throws {Error} If the environment variable is not set or path does not exist. + */ export function requireEnvPathExist(name: string): string -/** Get the common prefix of the two strings. +/** + * Get the common prefix of the two strings. * @param a - the first string. * @param b - the second string. - * @returns The common prefix. */ + * @returns The common prefix. + */ export function getCommonPrefix(a: string, b: string): string diff --git a/app/common/src/detect.ts b/app/common/src/detect.ts index df22a2a12581..f6f0fd428e6a 100644 --- a/app/common/src/detect.ts +++ b/app/common/src/detect.ts @@ -23,8 +23,10 @@ export enum Platform { android = 'Android', } -/** The platform the app is currently running on. - * This is used to determine whether `metaKey` or `ctrlKey` is used in shortcuts. */ +/** + * The platform the app is currently running on. + * This is used to determine whether `metaKey` or `ctrlKey` is used in shortcuts. + */ export function platform() { if (isOnWindowsPhone()) { // MUST be before Android and Windows. @@ -96,8 +98,10 @@ export enum Browser { opera = 'Opera', } -/** Return the platform the app is currently running on. - * This is used to determine whether `metaKey` or `ctrlKey` is used in shortcuts. */ +/** + * Return the platform the app is currently running on. + * This is used to determine whether `metaKey` or `ctrlKey` is used in shortcuts. + */ export function browser(): Browser { if (isOnElectron()) { return Browser.electron @@ -117,10 +121,12 @@ export function browser(): Browser { return Browser.unknown } } -/** Returns `true` if running in Electron, else `false`. +/** + * Returns `true` if running in Electron, else `false`. * This is used to determine whether to use a `MemoryRouter` (stores history in an array) * or a `BrowserRouter` (stores history in the path of the URL). - * It is also used to determine whether to send custom state to Amplify for a workaround. */ + * It is also used to determine whether to send custom state to Amplify for a workaround. + */ export function isOnElectron() { return /electron/i.test(navigator.userAgent) } diff --git a/app/common/src/gtag.ts b/app/common/src/gtag.ts index d921c6747407..5f02671e2096 100644 --- a/app/common/src/gtag.ts +++ b/app/common/src/gtag.ts @@ -17,7 +17,7 @@ window.dataLayer = window.dataLayer || [] export function gtag(_action: 'config' | 'event' | 'js' | 'set', ..._args: unknown[]) { // @ts-expect-error This is explicitly not given types as it is a mistake to acess this // anywhere else. - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, prefer-rest-params window.dataLayer.push(arguments) } @@ -27,7 +27,7 @@ export function event(name: string, params?: object) { } gtag('js', new Date()) -// eslint-disable-next-line @typescript-eslint/naming-convention +// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase gtag('set', 'linker', { accept_incoming: true }) gtag('config', GOOGLE_ANALYTICS_TAG) if (GOOGLE_ANALYTICS_TAG === 'G-CLTBJ37MDM') { diff --git a/app/common/src/index.d.ts b/app/common/src/index.d.ts index 69e541e772a0..d6604aa3e4e8 100644 --- a/app/common/src/index.d.ts +++ b/app/common/src/index.d.ts @@ -1,18 +1,22 @@ -/** @file This module contains metadata about the product and distribution, +/** + * @file This module contains metadata about the product and distribution, * and various other constants that are needed in multiple sibling packages. * * Code in this package is used by two or more sibling packages of this package. The code is defined * here when it is not possible for a sibling package to own that code without introducing a - * circular dependency in our packages. */ + * circular dependency in our packages. + */ // ======================== // === Product metadata === // ======================== -/** URL protocol scheme for deep links to authentication flow pages, without the `:` suffix. +/** + * URL protocol scheme for deep links to authentication flow pages, without the `:` suffix. * * For example: the deep link URL - * `enso://authentication/register?code=...&state=...` uses this scheme. */ + * `enso://authentication/register?code=...&state=...` uses this scheme. + */ export const DEEP_LINK_SCHEME: string /** Name of the product. */ @@ -21,12 +25,16 @@ export const PRODUCT_NAME: string /** Company name, used as the copyright holder. */ export const COMPANY_NAME: string -/** The domain on which the Cloud Dashboard web app is hosted. - * Excludes the protocol (`https://`). */ +/** + * The domain on which the Cloud Dashboard web app is hosted. + * Excludes the protocol (`https://`). + */ export const CLOUD_DASHBOARD_DOMAIN: string -/** COOP, COEP, and CORP headers: https://web.dev/coop-coep/ +/** + * COOP, COEP, and CORP headers: https://web.dev/coop-coep/ * * These are required to increase the resolution of `performance.now()` timers, - * making profiling a lot more accurate and consistent. */ + * making profiling a lot more accurate and consistent. + */ export const COOP_COEP_CORP_HEADERS: [header: string, value: string][] diff --git a/app/common/src/services/Backend.ts b/app/common/src/services/Backend.ts index f7658ad42212..6fd21e209ed6 100644 --- a/app/common/src/services/Backend.ts +++ b/app/common/src/services/Backend.ts @@ -29,8 +29,10 @@ export const UserGroupId = newtype.newtypeConstructor() export type DirectoryId = newtype.Newtype export const DirectoryId = newtype.newtypeConstructor() -/** Unique identifier for an asset representing the items inside a directory for which the - * request to retrive the items has not yet completed. */ +/** + * Unique identifier for an asset representing the items inside a directory for which the + * request to retrive the items has not yet completed. + */ export type LoadingAssetId = newtype.Newtype export const LoadingAssetId = newtype.newtypeConstructor() @@ -38,8 +40,10 @@ export const LoadingAssetId = newtype.newtypeConstructor() export type EmptyAssetId = newtype.Newtype export const EmptyAssetId = newtype.newtypeConstructor() -/** Unique identifier for an asset representing the nonexistent children of a directory - * that failed to fetch. */ +/** + * Unique identifier for an asset representing the nonexistent children of a directory + * that failed to fetch. + */ export type ErrorAssetId = newtype.Newtype export const ErrorAssetId = newtype.newtypeConstructor() @@ -129,14 +133,18 @@ export function isUserGroupId(id: string): id is UserGroupId { const PLACEHOLDER_USER_GROUP_PREFIX = 'usergroup-placeholder-' -/** Whether a given {@link UserGroupId} represents a user group that does not yet exist on the - * server. */ +/** + * Whether a given {@link UserGroupId} represents a user group that does not yet exist on the + * server. + */ export function isPlaceholderUserGroupId(id: string) { return id.startsWith(PLACEHOLDER_USER_GROUP_PREFIX) } -/** Return a new {@link UserGroupId} that represents a placeholder user group that is yet to finish - * being created on the backend. */ +/** + * Return a new {@link UserGroupId} that represents a placeholder user group that is yet to finish + * being created on the backend. + */ export function newPlaceholderUserGroupId() { return UserGroupId(`${PLACEHOLDER_USER_GROUP_PREFIX}${uniqueString.uniqueString()}`) } @@ -153,17 +161,21 @@ export enum BackendType { /** Metadata uniquely identifying a user inside an organization. */ export interface UserInfo { - /** The ID of the parent organization. If this is a sole user, they are implicitly in an - * organization consisting of only themselves. */ + /** + * The ID of the parent organization. If this is a sole user, they are implicitly in an + * organization consisting of only themselves. + */ readonly organizationId: OrganizationId /** The name of the parent organization. */ readonly organizationName?: string - /** The ID of this user. + /** + * The ID of this user. * * The user ID is globally unique. Thus, the user ID is always sufficient to uniquely identify a * user. The user ID is guaranteed to never change, once assigned. For these reasons, the user ID * should be the preferred way to uniquely refer to a user. That is, when referring to a user, - * prefer this field over `name`, `email`, `subject`, or any other mechanism, where possible. */ + * prefer this field over `name`, `email`, `subject`, or any other mechanism, where possible. + */ readonly userId: UserId readonly name: string readonly email: EmailAddress @@ -173,8 +185,10 @@ export interface UserInfo { /** A user in the application. These are the primary owners of a project. */ export interface User extends UserInfo { - /** If `false`, this account is awaiting acceptance from an administrator, and endpoints other than - * `usersMe` will not work. */ + /** + * If `false`, this account is awaiting acceptance from an administrator, and endpoints other than + * `usersMe` will not work. + */ readonly isEnabled: boolean readonly isOrganizationAdmin: boolean readonly rootDirectoryId: DirectoryId @@ -200,11 +214,15 @@ export enum ProjectState { provisioned = 'Provisioned', opened = 'Opened', closed = 'Closed', - /** A frontend-specific state, representing a project that should be displayed as - * `openInProgress`, but has not yet been added to the backend. */ + /** + * A frontend-specific state, representing a project that should be displayed as + * `openInProgress`, but has not yet been added to the backend. + */ placeholder = 'Placeholder', - /** A frontend-specific state, representing a project that should be displayed as `closed`, - * but is still in the process of shutting down. */ + /** + * A frontend-specific state, representing a project that should be displayed as `closed`, + * but is still in the process of shutting down. + */ closing = 'Closing', } @@ -374,11 +392,13 @@ export interface Label { readonly color: LChColor } -/** Type of application that a {@link Version} applies to. +/** + * Type of application that a {@link Version} applies to. * * We keep track of both backend and IDE versions, so that we can update the two independently. * However the format of the version numbers is the same for both, so we can use the same type for - * both. We just need this enum to disambiguate. */ + * both. We just need this enum to disambiguate. + */ export enum VersionType { backend = 'Backend', ide = 'Ide', @@ -566,7 +586,7 @@ export interface UpdatedDirectory { } /** The type returned from the "create directory" endpoint. */ -export interface Directory extends DirectoryAsset {} +export type Directory = DirectoryAsset /** The subset of asset fields returned by the "copy asset" endpoint. */ export interface CopiedAsset { @@ -711,8 +731,10 @@ export enum AssetType { secret = 'secret', datalink = 'datalink', directory = 'directory', - /** A special {@link AssetType} representing the unknown items of a directory, before the - * request to retrieve the items completes. */ + /** + * A special {@link AssetType} representing the unknown items of a directory, before the + * request to retrieve the items completes. + */ specialLoading = 'specialLoading', /** A special {@link AssetType} representing a directory listing that is empty. */ specialEmpty = 'specialEmpty', @@ -732,8 +754,10 @@ export interface IdType { readonly [AssetType.specialError]: ErrorAssetId } -/** Integers (starting from 0) corresponding to the order in which each asset type should appear - * in a directory listing. */ +/** + * Integers (starting from 0) corresponding to the order in which each asset type should appear + * in a directory listing. + */ export const ASSET_TYPE_ORDER: Readonly> = { // This is a sequence of numbers, not magic numbers. `1000` is an arbitrary number // that are higher than the number of possible asset types. @@ -753,22 +777,28 @@ export const ASSET_TYPE_ORDER: Readonly> = { // === Asset === // ============= -/** Metadata uniquely identifying a directory entry. - * These can be Projects, Files, Secrets, or other directories. */ +/** + * Metadata uniquely identifying a directory entry. + * These can be Projects, Files, Secrets, or other directories. + */ export interface BaseAsset { readonly id: AssetId readonly title: string readonly modifiedAt: dateTime.Rfc3339DateTime - /** This is defined as a generic {@link AssetId} in the backend, however it is more convenient - * (and currently safe) to assume it is always a {@link DirectoryId}. */ + /** + * This is defined as a generic {@link AssetId} in the backend, however it is more convenient + * (and currently safe) to assume it is always a {@link DirectoryId}. + */ readonly parentId: DirectoryId readonly permissions: readonly AssetPermission[] | null readonly labels: readonly LabelName[] | null readonly description: string | null } -/** Metadata uniquely identifying a directory entry. - * These can be Projects, Files, Secrets, or other directories. */ +/** + * Metadata uniquely identifying a directory entry. + * These can be Projects, Files, Secrets, or other directories. + */ export interface Asset extends BaseAsset { readonly type: Type readonly id: IdType[Type] @@ -776,31 +806,33 @@ export interface Asset extends BaseAsset { } /** A convenience alias for {@link Asset}<{@link AssetType.directory}>. */ -export interface DirectoryAsset extends Asset {} +export type DirectoryAsset = Asset /** A convenience alias for {@link Asset}<{@link AssetType.project}>. */ -export interface ProjectAsset extends Asset {} +export type ProjectAsset = Asset /** A convenience alias for {@link Asset}<{@link AssetType.file}>. */ -export interface FileAsset extends Asset {} +export type FileAsset = Asset /** A convenience alias for {@link Asset}<{@link AssetType.datalink}>. */ -export interface DatalinkAsset extends Asset {} +export type DatalinkAsset = Asset /** A convenience alias for {@link Asset}<{@link AssetType.secret}>. */ -export interface SecretAsset extends Asset {} +export type SecretAsset = Asset /** A convenience alias for {@link Asset}<{@link AssetType.specialLoading}>. */ -export interface SpecialLoadingAsset extends Asset {} +export type SpecialLoadingAsset = Asset /** A convenience alias for {@link Asset}<{@link AssetType.specialEmpty}>. */ -export interface SpecialEmptyAsset extends Asset {} +export type SpecialEmptyAsset = Asset /** A convenience alias for {@link Asset}<{@link AssetType.specialError}>. */ -export interface SpecialErrorAsset extends Asset {} +export type SpecialErrorAsset = Asset -/** Creates a {@link DirectoryAsset} representing the root directory for the organization, - * with all irrelevant fields initialized to default values. */ +/** + * Creates a {@link DirectoryAsset} representing the root directory for the organization, + * with all irrelevant fields initialized to default values. + */ export function createRootDirectoryAsset(directoryId: DirectoryId): DirectoryAsset { return { type: AssetType.directory, @@ -860,8 +892,10 @@ export function createPlaceholderProjectAsset( } } -/** Creates a {@link SpecialLoadingAsset}, with all irrelevant fields initialized to default - * values. */ +/** + * Creates a {@link SpecialLoadingAsset}, with all irrelevant fields initialized to default + * values. + */ export function createSpecialLoadingAsset(directoryId: DirectoryId): SpecialLoadingAsset { return { type: AssetType.specialLoading, @@ -876,8 +910,10 @@ export function createSpecialLoadingAsset(directoryId: DirectoryId): SpecialLoad } } -/** Creates a {@link SpecialEmptyAsset}, with all irrelevant fields initialized to default - * values. */ +/** + * Creates a {@link SpecialEmptyAsset}, with all irrelevant fields initialized to default + * values. + */ export function createSpecialEmptyAsset(directoryId: DirectoryId): SpecialEmptyAsset { return { type: AssetType.specialEmpty, @@ -892,8 +928,10 @@ export function createSpecialEmptyAsset(directoryId: DirectoryId): SpecialEmptyA } } -/** Creates a {@link SpecialErrorAsset}, with all irrelevant fields initialized to default - * values. */ +/** + * Creates a {@link SpecialErrorAsset}, with all irrelevant fields initialized to default + * values. + */ export function createSpecialErrorAsset(directoryId: DirectoryId): SpecialErrorAsset { return { type: AssetType.specialError, @@ -1011,8 +1049,10 @@ export interface AssetVersions { // === compareAssetPermissions === // =============================== -/** Return a positive number when `a > b`, a negative number when `a < b`, and `0` - * when `a === b`. */ +/** + * Return a positive number when `a > b`, a negative number when `a < b`, and `0` + * when `a === b`. + */ export function compareAssetPermissions(a: AssetPermission, b: AssetPermission) { const relativePermissionPrecedence = permissions.PERMISSION_ACTION_PRECEDENCE[a.permission] - @@ -1126,8 +1166,10 @@ export interface CreateProjectRequestBody { readonly datalinkId?: DatalinkId } -/** HTTP request body for the "update project" endpoint. - * Only updates of the `projectName` or `ami` are allowed. */ +/** + * HTTP request body for the "update project" endpoint. + * Only updates of the `projectName` or `ami` are allowed. + */ export interface UpdateProjectRequestBody { readonly projectName: string | null readonly ami: Ami | null @@ -1256,8 +1298,10 @@ export function compareAssets(a: AnyAsset, b: AnyAsset) { // === getAssetId === // ================== -/** A convenience function to get the `id` of an {@link Asset}. - * This is useful to avoid React re-renders as it is not re-created on each function call. */ +/** + * A convenience function to get the `id` of an {@link Asset}. + * This is useful to avoid React re-renders as it is not re-created on each function call. + */ export function getAssetId(asset: Asset) { return asset.id } @@ -1313,8 +1357,10 @@ export function stripProjectExtension(name: string) { return name.replace(/[.](?:tar[.]gz|zip|enso-project)$/, '') } -/** Return both the name and extension of the project file name (if any). - * Otherwise, returns the entire name as the basename. */ +/** + * Return both the name and extension of the project file name (if any). + * Otherwise, returns the entire name as the basename. + */ export function extractProjectExtension(name: string) { const [, basename, extension] = name.match(/^(.*)[.](tar[.]gz|zip|enso-project)$/) ?? [] return { basename: basename ?? name, extension: extension ?? '' } diff --git a/app/common/src/utilities/data/array.ts b/app/common/src/utilities/data/array.ts index b225b58ce6bc..392f7cdc37c4 100644 --- a/app/common/src/utilities/data/array.ts +++ b/app/common/src/utilities/data/array.ts @@ -15,15 +15,19 @@ export function shallowEqual(a: readonly T[], b: readonly T[]) { // === includes === // ================ -/** Returns a type predicate that returns true if and only if the value is in the array. - * The array MUST contain every element of `T`. */ +/** + * Returns a type predicate that returns true if and only if the value is in the array. + * The array MUST contain every element of `T`. + */ export function includes(array: T[], item: unknown): item is T { const arrayOfUnknown: unknown[] = array return arrayOfUnknown.includes(item) } -/** Returns a type predicate that returns true if and only if the value is in the iterable. - * The iterable MUST contain every element of `T`. */ +/** + * Returns a type predicate that returns true if and only if the value is in the iterable. + * The iterable MUST contain every element of `T`. + */ export function includesPredicate(array: Iterable) { const set: Set = array instanceof Set ? array : new Set(array) return (item: unknown): item is T => set.has(item) @@ -36,32 +40,40 @@ export function includesPredicate(array: Iterable) { /** The value returned when {@link Array.findIndex} fails. */ const NOT_FOUND = -1 -/** Insert items before the first index `i` for which `predicate(array[i])` is `true`. - * Insert the items at the end if the `predicate` never returns `true`. */ +/** + * Insert items before the first index `i` for which `predicate(array[i])` is `true`. + * Insert the items at the end if the `predicate` never returns `true`. + */ export function spliceBefore(array: T[], items: T[], predicate: (value: T) => boolean) { const index = array.findIndex(predicate) array.splice(index === NOT_FOUND ? array.length : index, 0, ...items) return array } -/** Return a copy of the array, with items inserted before the first index `i` for which +/** + * Return a copy of the array, with items inserted before the first index `i` for which * `predicate(array[i])` is `true`. The items are inserted at the end if the `predicate` never - * returns `true`. */ + * returns `true`. + */ export function splicedBefore(array: T[], items: T[], predicate: (value: T) => boolean) { return spliceBefore(Array.from(array), items, predicate) } -/** Insert items after the first index `i` for which `predicate(array[i])` is `true`. - * Insert the items at the end if the `predicate` never returns `true`. */ +/** + * Insert items after the first index `i` for which `predicate(array[i])` is `true`. + * Insert the items at the end if the `predicate` never returns `true`. + */ export function spliceAfter(array: T[], items: T[], predicate: (value: T) => boolean) { const index = array.findIndex(predicate) array.splice(index === NOT_FOUND ? array.length : index + 1, 0, ...items) return array } -/** Return a copy of the array, with items inserted after the first index `i` for which +/** + * Return a copy of the array, with items inserted after the first index `i` for which * `predicate(array[i])` is `true`. The items are inserted at the end if the `predicate` never - * returns `true`. */ + * returns `true`. + */ export function splicedAfter(array: T[], items: T[], predicate: (value: T) => boolean) { return spliceAfter(Array.from(array), items, predicate) } diff --git a/app/common/src/utilities/data/dateTime.ts b/app/common/src/utilities/data/dateTime.ts index 2ac9f84e9c99..1a009569ae8e 100644 --- a/app/common/src/utilities/data/dateTime.ts +++ b/app/common/src/utilities/data/dateTime.ts @@ -35,8 +35,10 @@ export type Rfc3339DateTime = newtype.Newtype // eslint-disable-next-line @typescript-eslint/no-redeclare export const Rfc3339DateTime = newtype.newtypeConstructor() -/** Return a new {@link Date} with units below days (hours, minutes, seconds and milliseconds) - * set to `0`. */ +/** + * Return a new {@link Date} with units below days (hours, minutes, seconds and milliseconds) + * set to `0`. + */ export function toDate(dateTime: Date) { return new Date(dateTime.getFullYear(), dateTime.getMonth(), dateTime.getDate()) } diff --git a/app/common/src/utilities/data/newtype.ts b/app/common/src/utilities/data/newtype.ts index 9ecb616fd571..b6add8ced749 100644 --- a/app/common/src/utilities/data/newtype.ts +++ b/app/common/src/utilities/data/newtype.ts @@ -12,8 +12,10 @@ type NewtypeVariant = { readonly _$type: TypeName } -/** An interface specifying the variant of a newtype, where the discriminator is mutable. - * This is safe, as the discriminator should be a string literal type anyway. */ +/** + * An interface specifying the variant of a newtype, where the discriminator is mutable. + * This is safe, as the discriminator should be a string literal type anyway. + */ // This is required for compatibility with the dependency `enso-chat`. // eslint-disable-next-line no-restricted-syntax type MutableNewtypeVariant = { @@ -21,7 +23,8 @@ type MutableNewtypeVariant = { _$type: TypeName } -/** Used to create a "branded type", +/** + * Used to create a "branded type", * which contains a property that only exists at compile time. * * `Newtype` and `Newtype` are not compatible with each other, @@ -33,11 +36,14 @@ type MutableNewtypeVariant = { * It is similar to a `newtype` in other languages. * Note however because TypeScript is structurally typed, * a branded type is assignable to its base type: - * `a: string = asNewtype>(b)` successfully typechecks. */ + * `a: string = asNewtype>(b)` successfully typechecks. + */ export type Newtype = NewtypeVariant & T -/** Extracts the original type out of a {@link Newtype}. - * Its only use is in {@link newtypeConstructor}. */ +/** + * Extracts the original type out of a {@link Newtype}. + * Its only use is in {@link newtypeConstructor}. + */ type UnNewtype> = T extends infer U & NewtypeVariant ? U extends infer V & MutableNewtypeVariant ? @@ -51,9 +57,11 @@ type NotNewtype = { readonly _$type?: never } -/** Converts a value that is not a newtype, to a value that is a newtype. +/** + * Converts a value that is not a newtype, to a value that is a newtype. * This function intentionally returns another function, to ensure that each function instance - * is only used for one type, avoiding the de-optimization caused by polymorphic functions. */ + * is only used for one type, avoiding the de-optimization caused by polymorphic functions. + */ export function newtypeConstructor>() { // This cast is unsafe. // `T` has an extra property `_$type` which is used purely for typechecking diff --git a/app/common/src/utilities/data/object.ts b/app/common/src/utilities/data/object.ts index 1e7e7a2a1071..10dd6186f929 100644 --- a/app/common/src/utilities/data/object.ts +++ b/app/common/src/utilities/data/object.ts @@ -16,8 +16,10 @@ export type Mutable = { /** Prevents generic parameter inference by hiding the type parameter behind a conditional type. */ type NoInfer = [T][T extends T ? 0 : never] -/** Immutably shallowly merge an object with a partial update. - * Does not preserve classes. Useful for preserving order of properties. */ +/** + * Immutably shallowly merge an object with a partial update. + * Does not preserve classes. Useful for preserving order of properties. + */ export function merge(object: T, update: Partial): T { for (const [key, value] of Object.entries(update)) { // eslint-disable-next-line no-restricted-syntax @@ -57,8 +59,10 @@ export function unsafeMutable(object: T): { -readonly [K in ke // === unsafeEntries === // ===================== -/** Return the entries of an object. UNSAFE only when it is possible for an object to have - * extra keys. */ +/** + * Return the entries of an object. UNSAFE only when it is possible for an object to have + * extra keys. + */ export function unsafeEntries( object: T, ): readonly { [K in keyof T]: readonly [K, T[K]] }[keyof T][] { @@ -83,8 +87,10 @@ export function unsafeRemoveUndefined( // === mapEntries === // ================== -/** Return the entries of an object. UNSAFE only when it is possible for an object to have - * extra keys. */ +/** + * Return the entries of an object. UNSAFE only when it is possible for an object to have + * extra keys. + */ export function mapEntries( object: Record, map: (key: K, value: V) => W, diff --git a/app/common/src/utilities/permissions.ts b/app/common/src/utilities/permissions.ts index d9bc2aa3defc..7404691de581 100644 --- a/app/common/src/utilities/permissions.ts +++ b/app/common/src/utilities/permissions.ts @@ -111,8 +111,10 @@ export const FROM_PERMISSION_ACTION: Readonly> = { [Permission.owner]: PermissionAction.own, [Permission.admin]: PermissionAction.admin, @@ -123,8 +125,10 @@ export const TYPE_TO_PERMISSION_ACTION: Readonly> = { [Permission.owner]: 'ownerPermissionType', [Permission.admin]: 'adminPermissionType', @@ -181,13 +185,13 @@ interface BasePermissions { } /** Owner permissions for an asset. */ -interface OwnerPermissions extends BasePermissions {} +type OwnerPermissions = BasePermissions /** Admin permissions for an asset. */ -interface AdminPermissions extends BasePermissions {} +type AdminPermissions = BasePermissions /** Editor permissions for an asset. */ -interface EditPermissions extends BasePermissions {} +type EditPermissions = BasePermissions /** Reader permissions for an asset. */ interface ReadPermissions extends BasePermissions { diff --git a/app/common/src/utilities/style/__tests__/tabBar.test.ts b/app/common/src/utilities/style/__tests__/tabBar.test.ts index 75101edc7638..205896c5b78a 100644 --- a/app/common/src/utilities/style/__tests__/tabBar.test.ts +++ b/app/common/src/utilities/style/__tests__/tabBar.test.ts @@ -42,7 +42,7 @@ const guiTabCases = [ v.test.each([ { group: 'Dashboard', cases: dashboardTabCases }, { group: 'GUI', cases: guiTabCases }, -])('Tab clip path: $group', ({ group, cases }) => { +])('Tab clip path: $group', ({ cases }) => { cases.forEach(({ input, expected }) => { const result = tabBar.tabClipPath(input.bounds, input.radius, (input as TabClipPathInput)?.side) v.expect(result).toBe(expected) diff --git a/app/common/tsconfig.json b/app/common/tsconfig.json index 4db43171dfe4..4837545253f3 100644 --- a/app/common/tsconfig.json +++ b/app/common/tsconfig.json @@ -2,8 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "lib": ["DOM", "es2023"], - "allowJs": false, - "checkJs": false, + "allowJs": true, + "checkJs": true, "skipLibCheck": false }, "include": ["./src/", "./src/text/english.json", "../types/"] diff --git a/app/gui/src/dashboard/hooks/billing/FeaturesConfiguration.ts b/app/gui/src/dashboard/hooks/billing/FeaturesConfiguration.ts index b9bc0d5fb52c..f031c56be87e 100644 --- a/app/gui/src/dashboard/hooks/billing/FeaturesConfiguration.ts +++ b/app/gui/src/dashboard/hooks/billing/FeaturesConfiguration.ts @@ -62,7 +62,7 @@ export const PAYWALL_LEVELS: Record = { } /** - * + * Possible paywall unlock states for a user account. */ export type PaywallLevel = (typeof PAYWALL_LEVELS)[keyof typeof PAYWALL_LEVELS] diff --git a/app/gui/src/dashboard/hooks/debounceCallbackHooks.ts b/app/gui/src/dashboard/hooks/debounceCallbackHooks.ts index 720258d50b82..8d085c99026a 100644 --- a/app/gui/src/dashboard/hooks/debounceCallbackHooks.ts +++ b/app/gui/src/dashboard/hooks/debounceCallbackHooks.ts @@ -84,8 +84,9 @@ export function useDebouncedCallback unknown>( } /** - * + * The type of a wrapped function that has been debounced. */ -export interface DebouncedFunction unknown> { - (this: ThisParameterType, ...args: Parameters): void -} +export type DebouncedFunction unknown> = ( + this: ThisParameterType, + ...args: Parameters +) => void diff --git a/app/gui/src/dashboard/modules/payments/api/useSubscriptionPrice.ts b/app/gui/src/dashboard/modules/payments/api/useSubscriptionPrice.ts index 4d4632dcfcb2..1b570f08a15a 100644 --- a/app/gui/src/dashboard/modules/payments/api/useSubscriptionPrice.ts +++ b/app/gui/src/dashboard/modules/payments/api/useSubscriptionPrice.ts @@ -1,7 +1,8 @@ /** * @file * - * This file contains the `useSubscriptionPrice` hook that is used to fetch the subscription price based on the provided parameters. + * The `useSubscriptionPrice` hook that is used to fetch the subscription price + * based on the provided parameters. */ import { queryOptions, useQuery } from '@tanstack/react-query' @@ -10,9 +11,9 @@ import type { Plan } from '#/services/Backend' import { DISCOUNT_MULTIPLIER_BY_DURATION, PRICE_BY_PLAN } from '../constants' /** - * + * Options for {@link createSubscriptionPriceQuery}. */ -export interface SubscriptionPriceQueryParams { +export interface SubscriptionPriceQueryOptions { readonly plan: Plan readonly seats: number readonly period: number @@ -21,13 +22,13 @@ export interface SubscriptionPriceQueryParams { /** * Creates a query to fetch the subscription price based on the provided parameters. */ -export function createSubscriptionPriceQuery(params: SubscriptionPriceQueryParams) { +export function createSubscriptionPriceQuery(options: SubscriptionPriceQueryOptions) { return queryOptions({ - queryKey: ['getPrice', params] as const, + queryKey: ['getPrice', options] as const, queryFn: ({ queryKey }) => { const [, { seats, period, plan }] = queryKey - const discountMultiplier = DISCOUNT_MULTIPLIER_BY_DURATION[params.period] ?? 1 + const discountMultiplier = DISCOUNT_MULTIPLIER_BY_DURATION[options.period] ?? 1 const fullPrice = PRICE_BY_PLAN[plan] const price = fullPrice * discountMultiplier const discount = fullPrice - price @@ -46,6 +47,6 @@ export function createSubscriptionPriceQuery(params: SubscriptionPriceQueryParam /** * Fetches the subscription price based on the provided parameters. */ -export function useSubscriptionPrice(params: SubscriptionPriceQueryParams) { +export function useSubscriptionPrice(params: SubscriptionPriceQueryOptions) { return useQuery(createSubscriptionPriceQuery(params)) } diff --git a/app/gui/src/project-view/assets/font-dejavu.css b/app/gui/src/project-view/assets/font-dejavu.css index e070d086f662..a4acbe007856 100644 --- a/app/gui/src/project-view/assets/font-dejavu.css +++ b/app/gui/src/project-view/assets/font-dejavu.css @@ -9,4 +9,3 @@ src: url('/font-dejavu/DejaVuSansMono-Bold.ttf'); font-weight: 700; } - diff --git a/app/gui/src/project-view/assets/font-enso.css b/app/gui/src/project-view/assets/font-enso.css index d65196d18094..0a9a259eda9d 100644 --- a/app/gui/src/project-view/assets/font-enso.css +++ b/app/gui/src/project-view/assets/font-enso.css @@ -51,4 +51,3 @@ src: url('/font-enso/Enso-Black.ttf'); font-weight: 900; } - diff --git a/app/gui/src/project-view/assets/font-mplus1.css b/app/gui/src/project-view/assets/font-mplus1.css index b2a98be0bf23..253d7e638f37 100644 --- a/app/gui/src/project-view/assets/font-mplus1.css +++ b/app/gui/src/project-view/assets/font-mplus1.css @@ -2,4 +2,3 @@ font-family: 'M PLUS 1'; src: url('/font-mplus1/MPLUS1[wght].ttf'); } - diff --git a/app/ide-desktop/client/dist.ts b/app/ide-desktop/client/dist.ts index 3c717a2e9c0d..195806c8d271 100644 --- a/app/ide-desktop/client/dist.ts +++ b/app/ide-desktop/client/dist.ts @@ -1,4 +1,5 @@ -/** @file This script creates a packaged IDE distribution using electron-builder. +/** + * @file This script creates a packaged IDE distribution using electron-builder. * Behaviour details are controlled by the environment variables or CLI arguments. * @see electronBuilderConfig.Arguments */ diff --git a/app/ide-desktop/client/electron-builder-config.ts b/app/ide-desktop/client/electron-builder-config.ts index 6b53730c32ff..55862095e7ce 100644 --- a/app/ide-desktop/client/electron-builder-config.ts +++ b/app/ide-desktop/client/electron-builder-config.ts @@ -1,9 +1,11 @@ -/** @file This module defines a TS script that is responsible for invoking the Electron Builder +/** + * @file This module defines a TS script that is responsible for invoking the Electron Builder * process to bundle the entire IDE distribution. * * There are two areas to this: * - Parsing CLI options as per our needs. - * - The default configuration of the build process. */ + * - The default configuration of the build process. + */ /** @module */ import * as childProcess from 'node:child_process' @@ -26,8 +28,10 @@ import BUILD_INFO from './buildInfo' // === Types === // ============= -/** The parts of the electron-builder configuration that we want to keep configurable. - * @see `args` definition below for fields description. */ +/** + * The parts of the electron-builder configuration that we want to keep configurable. + * @see `args` definition below for fields description. + */ export interface Arguments { // The types come from a third-party API and cannot be changed. // eslint-disable-next-line no-restricted-syntax @@ -42,9 +46,11 @@ export interface Arguments { /** File association configuration, extended with information needed by the `enso-installer`. */ interface ExtendedFileAssociation extends electronBuilder.FileAssociation { - /** The Windows registry key under which the file association is registered. + /** + * The Windows registry key under which the file association is registered. * - * Should follow the pattern `Enso.CamelCaseName`. */ + * Should follow the pattern `Enso.CamelCaseName`. + */ readonly progId: string } @@ -61,8 +67,10 @@ interface InstallerAdditionalConfig { // === Argument parser configuration === //====================================== -/** CLI argument parser (with support for environment variables) that provides - * the necessary options. */ +/** + * CLI argument parser (with support for environment variables) that provides + * the necessary options. + */ export const args: Arguments = await yargs(process.argv.slice(2)) .env('ENSO_BUILD') .option({ @@ -132,10 +140,12 @@ export const EXTENDED_FILE_ASSOCIATIONS = [ }, ] -/** Returns non-extended file associations, as required by the `electron-builder`. +/** + * Returns non-extended file associations, as required by the `electron-builder`. * * Note that we need to actually remove any additional fields that we added to the file associations, - * as the `electron-builder` will error out if it encounters unknown fields. */ + * as the `electron-builder` will error out if it encounters unknown fields. + */ function getFileAssociations(): electronBuilder.FileAssociation[] { return EXTENDED_FILE_ASSOCIATIONS.map(assoc => { const { ext, name, role, mimeType } = assoc @@ -177,7 +187,8 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil // simplified for the MSI installer to cope. artifactName: 'enso-${os}-${arch}-' + BUILD_INFO.version + '.${ext}', - /** Definitions of URL {@link electronBuilder.Protocol} schemes used by the IDE. + /** + * Definitions of URL {@link electronBuilder.Protocol} schemes used by the IDE. * * Electron will register all URL protocol schemes defined here with the OS. * Once a URL protocol scheme is registered with the OS, any links using that scheme @@ -191,7 +202,8 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil * - navigate to the location specified by the URL of the deep link. * * For details on how this works, see: - * https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app. */ + * https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app. + */ protocols: [ /** Electron URL protocol scheme definition for deep links to authentication pages. */ { @@ -336,7 +348,8 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil } } -/** Write the configuration to a JSON file. +/** + * Write the configuration to a JSON file. * * On Windows it is necessary to provide configuration to our installer. On other platforms, this may be useful for debugging. * diff --git a/app/ide-desktop/client/esbuildConfig.ts b/app/ide-desktop/client/esbuildConfig.ts index 672c15866f50..520abdd1792e 100644 --- a/app/ide-desktop/client/esbuildConfig.ts +++ b/app/ide-desktop/client/esbuildConfig.ts @@ -18,7 +18,8 @@ await appConfig.readEnvironmentFromFile() // === Bundling === // ================ -/** Get the bundler options using the environment. +/** + * Get the bundler options using the environment. * * The following environment variables are required: * - `ENSO_BUILD_IDE` - output directory for bundled client files; diff --git a/app/ide-desktop/client/paths.ts b/app/ide-desktop/client/paths.ts index 434cd14fc235..498022a15cf1 100644 --- a/app/ide-desktop/client/paths.ts +++ b/app/ide-desktop/client/paths.ts @@ -6,8 +6,10 @@ import * as buildUtils from 'enso-common/src/buildUtils' // === Paths to resources === // ========================== -/** Path to the Project Manager bundle within the electron distribution - * (relative to electron's resources directory). */ +/** + * Path to the Project Manager bundle within the electron distribution + * (relative to electron's resources directory). + */ export const PROJECT_MANAGER_BUNDLE = 'enso' /** Distribution directory for IDE. */ diff --git a/app/ide-desktop/client/src/authentication.ts b/app/ide-desktop/client/src/authentication.ts index ebc92be50038..f4582a415a8c 100644 --- a/app/ide-desktop/client/src/authentication.ts +++ b/app/ide-desktop/client/src/authentication.ts @@ -1,4 +1,5 @@ -/** @file Definition of the Electron-specific parts of the authentication flows of the IDE. +/** + * @file Definition of the Electron-specific parts of the authentication flows of the IDE. * * # Overview of Authentication/Authorization * @@ -69,7 +70,8 @@ * - The {@link ipc.Channel.openDeepLink} listener registered by the dashboard receives the event. * Then it parses the {@link URL} from the event's {@link URL} argument. Then it uses the * {@link URL} to redirect the user to the dashboard, to the page specified in the {@link URL}'s - * `pathname`. */ + * `pathname`. + */ import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' @@ -90,12 +92,14 @@ const logger = contentConfig.logger // === Initialize Authentication Module === // ======================================== -/** Configure all the functionality that must be set up in the Electron app to support +/** + * Configure all the functionality that must be set up in the Electron app to support * authentication-related flows. Must be called in the Electron app `whenReady` event. * @param window - A function that returns the main Electron window. This argument is a lambda and * not a variable because the main window is not available when this function is called. This module * does not use the `window` until after it is initialized, so while the lambda may return `null` in - * theory, it never will in practice. */ + * theory, it never will in practice. + */ export function initAuthentication(window: () => electron.BrowserWindow) { // Listen for events to open a URL externally in a browser the user trusts. This is used for // OAuth authentication, both for trustworthiness and for convenience (the ability to use the @@ -143,13 +147,13 @@ export function initAuthentication(window: () => electron.BrowserWindow) { fs.writeFile( path.join(credentialsHomePath, credentialsFileName), JSON.stringify({ - /* eslint-disable @typescript-eslint/naming-convention */ + /* eslint-disable @typescript-eslint/naming-convention, camelcase */ client_id: accessTokenPayload.clientId, access_token: accessTokenPayload.accessToken, refresh_token: accessTokenPayload.refreshToken, refresh_url: accessTokenPayload.refreshUrl, expire_at: accessTokenPayload.expireAt, - /* eslint-enable @typescript-eslint/naming-convention */ + /* eslint-enable @typescript-eslint/naming-convention, camelcase */ }), innerError => { if (innerError) { diff --git a/app/ide-desktop/client/src/config.ts b/app/ide-desktop/client/src/config.ts index 8781c74a2cdb..14ff6821aad2 100644 --- a/app/ide-desktop/client/src/config.ts +++ b/app/ide-desktop/client/src/config.ts @@ -1,5 +1,7 @@ -/** @file Configuration of the application. It extends the web application configuration with - * Electron-specific options. */ +/** + * @file Configuration of the application. It extends the web application configuration with + * Electron-specific options. + */ import chalk from 'chalk' @@ -557,7 +559,7 @@ export const CONFIG = contentConfig.OPTIONS.merge( }), // Please note that this option uses the snake-case naming convention because // Chrome defines it so. - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase force_high_performance_gpu: new contentConfig.Option({ passToWebApplication: false, primary: false, @@ -566,7 +568,7 @@ export const CONFIG = contentConfig.OPTIONS.merge( }), // Please note that this option uses the snake-case naming convention because // Chrome defines it so. - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase force_low_power_gpu: new contentConfig.Option({ passToWebApplication: false, primary: false, diff --git a/app/ide-desktop/client/src/configParser.ts b/app/ide-desktop/client/src/configParser.ts index e2d0847d5fe4..9f29c29ddbfb 100644 --- a/app/ide-desktop/client/src/configParser.ts +++ b/app/ide-desktop/client/src/configParser.ts @@ -37,8 +37,10 @@ const USAGE = `the application from a web-browser, the creation of a window can be suppressed by ` + `entering either '-window=false' or '-no-window'.` -/** Contains information for a category of command line options and the options - * it is comprised of. */ +/** + * Contains information for a category of command line options and the options + * it is comprised of. + */ class Section { description = '' entries: (readonly [cmdOption: string, option: config.Option])[] = [] @@ -51,7 +53,8 @@ interface PrintHelpConfig { readonly helpExtended: boolean } -/** Command line help printer. The `groupsOrdering` parameter specifies the order in which the +/** + * Command line help printer. The `groupsOrdering` parameter specifies the order in which the * option groups should be printed. Groups not specified will be printed in the definition order. * * We use a custom help printer because Yargs has several issues: @@ -60,7 +63,8 @@ interface PrintHelpConfig { * 3. Every option has a `[type`] annotation and there is no API to disable it. * 4. There is no option to print commands with single dash instead of double-dash. * 5. Help coloring is not supported, and they do not want to support it: - * https://github.com/yargs/yargs/issues/251. */ + * https://github.com/yargs/yargs/issues/251. + */ function printHelp(cfg: PrintHelpConfig) { console.log(USAGE) const totalWidth = logger.terminalWidth() ?? DEFAULT_TERMINAL_WIDTH @@ -146,8 +150,10 @@ function printHelp(cfg: PrintHelpConfig) { } } -/** Wrap the text to a specific output width. If a word is longer than the output width, it will be - * split. */ +/** + * Wrap the text to a specific output width. If a word is longer than the output width, it will be + * split. + */ function wordWrap(str: string, width: number): string[] { if (width <= 0) { logger.error(`Cannot perform word wrap. The output width is set to '${width}'.`) @@ -217,8 +223,10 @@ export class ChromeOption { } } -/** Replace `-no-...` with `--no-...`. This is a hotfix for a Yargs bug: - * https://github.com/yargs/yargs-parser/issues/468. */ +/** + * Replace `-no-...` with `--no-...`. This is a hotfix for a Yargs bug: + * https://github.com/yargs/yargs-parser/issues/468. + */ function fixArgvNoPrefix(argv: readonly string[]): readonly string[] { const singleDashPrefix = '-no-' const doubleDashPrefix = '--no-' @@ -237,8 +245,10 @@ interface ArgvAndChromeOptions { readonly chromeOptions: ChromeOption[] } -/** Parse the given list of arguments into two distinct sets: regular arguments and those specific - * to Chrome. */ +/** + * Parse the given list of arguments into two distinct sets: regular arguments and those specific + * to Chrome. + */ function argvAndChromeOptions(processArgs: readonly string[]): ArgvAndChromeOptions { const chromeOptionRegex = /--?chrome.([^=]*)(?:=(.*))?/ const argv = [] diff --git a/app/ide-desktop/client/src/fileAssociations.ts b/app/ide-desktop/client/src/fileAssociations.ts index bede7f3061ba..27cd32ad1d18 100644 --- a/app/ide-desktop/client/src/fileAssociations.ts +++ b/app/ide-desktop/client/src/fileAssociations.ts @@ -1,9 +1,11 @@ -/** @file +/** + * @file * This module provides functionality for handling file opening events in the Enso IDE. * * It includes utilities for determining if a file can be opened, managing the file opening * process, and launching new instances of the IDE when necessary. The module also exports - * constants related to file associations and project handling. */ + * constants related to file associations and project handling. + */ import * as fsSync from 'node:fs' import * as pathModule from 'node:path' import process from 'node:process' @@ -39,13 +41,15 @@ export const SOURCE_FILE_SUFFIX = fileAssociations.SOURCE_FILE_SUFFIX // === Arguments Handling === // ========================== -/** Check if the given list of application startup arguments denotes an attempt to open a file. +/** + * Check if the given list of application startup arguments denotes an attempt to open a file. * * For example, this happens when the user double-clicks on a file in the file explorer and the * application is launched with the file path as an argument. * @param clientArgs - A list of arguments passed to the application, stripped from the initial * executable name and any electron dev mode arguments. - * @returns The path to the file to open, or `null` if no file was specified. */ + * @returns The path to the file to open, or `null` if no file was specified. + */ export function argsDenoteFileOpenAttempt(clientArgs: readonly string[]): string | null { const arg = clientArgs[0] let result: string | null = null @@ -117,9 +121,11 @@ export function onFileOpened(event: electron.Event, path: string): string | null } } -/** Set up the `open-file` event handler that might import a project and invoke the given callback, +/** + * Set up the `open-file` event handler that might import a project and invoke the given callback, * if this IDE instance should load the project. See {@link onFileOpened} for more details. - * @param setProjectToOpen - A function that will be called with the path of the project to open. */ + * @param setProjectToOpen - A function that will be called with the path of the project to open. + */ export function setOpenFileEventHandler(setProjectToOpen: (path: string) => void) { electron.app.on('open-file', (_event, path) => { logger.log(`Opening file '${path}'.`) @@ -149,13 +155,15 @@ export function setOpenFileEventHandler(setProjectToOpen: (path: string) => void }) } -/** Handle the case where IDE is invoked with a file to open. +/** + * Handle the case where IDE is invoked with a file to open. * * Imports project if necessary. Returns the ID of the project to open. In case of an error, * the error message is displayed and the error is re-thrown. * @param openedFile - The path to the file to open. * @returns The ID of the project to open. - * @throws {Error} if the project from the file cannot be opened or imported. */ + * @throws {Error} if the project from the file cannot be opened or imported. + */ export function handleOpenFile(openedFile: string): project.ProjectInfo { try { return project.importProjectFromPath(openedFile) diff --git a/app/ide-desktop/client/src/globals.d.ts b/app/ide-desktop/client/src/globals.d.ts index 05ed085fb59e..b9c41639fbcc 100644 --- a/app/ide-desktop/client/src/globals.d.ts +++ b/app/ide-desktop/client/src/globals.d.ts @@ -1,8 +1,8 @@ -/** @file Globals defined outside of TypeScript files. +/** + * @file Globals defined outside of TypeScript files. * These are from variables defined at build time, environment variables, - * monkeypatching on `window` and generated code. */ -import type * as common from 'enso-common' - + * monkeypatching on `window` and generated code. + */ // This file is being imported for its types. // eslint-disable-next-line no-restricted-syntax import * as buildJson from './../../build.json' assert { type: 'json' } @@ -25,8 +25,10 @@ interface Enso { // === Backend API === // =================== -/** `window.backendApi` is a context bridge to the main process, when we're running in an - * Electron context. It contains non-authentication-related functionality. */ +/** + * `window.backendApi` is a context bridge to the main process, when we're running in an + * Electron context. It contains non-authentication-related functionality. + */ interface BackendApi { /** Return the ID of the new project. */ readonly importProjectFromPath: ( @@ -40,7 +42,8 @@ interface BackendApi { // === Authentication API === // ========================== -/** `window.authenticationApi` is a context bridge to the main process, when we're running in an +/** + * `window.authenticationApi` is a context bridge to the main process, when we're running in an * Electron context. * * # Safety @@ -48,12 +51,15 @@ interface BackendApi { * We're assuming that the main process has exposed the `authenticationApi` context bridge (see * `lib/client/src/preload.ts` for details), and that it contains the functions defined in this * interface. Our app can't function if these assumptions are not met, so we're disabling the - * TypeScript checks for this interface when we use it. */ + * TypeScript checks for this interface when we use it. + */ interface AuthenticationApi { /** Open a URL in the system browser. */ readonly openUrlInSystemBrowser: (url: string) => void - /** Set the callback to be called when the system browser redirects back to a URL in the app, - * via a deep link. See `setDeepLinkHandler` for details. */ + /** + * Set the callback to be called when the system browser redirects back to a URL in the app, + * via a deep link. See `setDeepLinkHandler` for details. + */ readonly setDeepLinkHandler: (callback: (url: string) => void) => void /** Saves the access token to a file. */ readonly saveAccessToken: (accessToken: dashboard.AccessToken | null) => void @@ -63,8 +69,10 @@ interface AuthenticationApi { // === Navigation API === // ====================== -/** `window.navigationApi` is a context bridge to the main process, when we're running in an - * Electron context. It contains navigation-related functionality. */ +/** + * `window.navigationApi` is a context bridge to the main process, when we're running in an + * Electron context. It contains navigation-related functionality. + */ interface NavigationApi { /** Go back in the navigation history. */ readonly goBack: () => void @@ -115,8 +123,10 @@ interface ProjectInfo { readonly parentDirectory: string } -/** `window.projectManagementApi` exposes functionality related to system events related to - * project management. */ +/** + * `window.projectManagementApi` exposes functionality related to system events related to + * project management. + */ interface ProjectManagementApi { readonly setOpenProjectHandler: (handler: (projectInfo: ProjectInfo) => void) => void } diff --git a/app/ide-desktop/client/src/index.ts b/app/ide-desktop/client/src/index.ts index 6bec836eb5bc..37ccfa46d514 100644 --- a/app/ide-desktop/client/src/index.ts +++ b/app/ide-desktop/client/src/index.ts @@ -198,7 +198,7 @@ class App { if (urlToOpen != null) { urlAssociations.handleOpenUrl(urlToOpen) } - } catch (e) { + } catch { // If we failed to open the file, we should enter the usual welcome screen. // The `handleOpenFile` function will have already displayed an error message. } diff --git a/app/ide-desktop/client/src/ipc.ts b/app/ide-desktop/client/src/ipc.ts index 996458bdffac..e0e0b7ef3cb3 100644 --- a/app/ide-desktop/client/src/ipc.ts +++ b/app/ide-desktop/client/src/ipc.ts @@ -1,5 +1,7 @@ -/** @file Inter-Process communication configuration of the application. IPC allows the web-view - * content to exchange information with the Electron application. */ +/** + * @file Inter-Process communication configuration of the application. IPC allows the web-view + * content to exchange information with the Electron application. + */ // =============== // === Channel === diff --git a/app/ide-desktop/client/src/log.ts b/app/ide-desktop/client/src/log.ts index 237046f740e6..18899fc0b9d8 100644 --- a/app/ide-desktop/client/src/log.ts +++ b/app/ide-desktop/client/src/log.ts @@ -1,10 +1,12 @@ -/** @file Logging utilities. +/** + * @file Logging utilities. * * This module includes a special {@link addFileLog function} that adds a new log consumer that * writes to a file. * * This is the primary entry point, though its building blocks are also exported, - * like {@link FileConsumer}. */ + * like {@link FileConsumer}. + */ import * as fsSync from 'node:fs' import * as pathModule from 'node:path' @@ -18,12 +20,14 @@ import * as paths from '@/paths' // === Log File === // ================ -/** Adds a new log consumer that writes to a file. +/** + * Adds a new log consumer that writes to a file. * * The path of the log file is {@link generateUniqueLogFileName automatically generated}. * * The log file is created in the {@link paths.LOGS_DIRECTORY logs directory} - * @returns The full path of the log file. */ + * @returns The full path of the log file. + */ export function addFileLog(): string { const dirname = paths.LOGS_DIRECTORY const filename = generateUniqueLogFileName() @@ -33,8 +37,10 @@ export function addFileLog(): string { return logFilePath } -/** Generate a unique log file name based on the current timestamp. - * @returns The file name log file. */ +/** + * Generate a unique log file name based on the current timestamp. + * @returns The file name log file. + */ export function generateUniqueLogFileName(): string { // Replace ':' with '-' because ':' is not allowed in file names. const timestamp = new Date().toISOString().replace(/:/g, '-') @@ -51,8 +57,10 @@ export class FileConsumer extends linkedDist.log.Consumer { private readonly logFilePath: string private readonly logFileHandle: number - /** Create a log consumer that writes to a file. - * @param logPath - The path of the log file. Must be writeable. */ + /** + * Create a log consumer that writes to a file. + * @param logPath - The path of the log file. Must be writeable. + */ constructor(logPath: string) { super() // Create the directory if it doesn't exist, otherwise fsSync.openSync will fail. @@ -85,8 +93,10 @@ export class FileConsumer extends linkedDist.log.Consumer { this.message('log', '[GROUP START]', ...args) } - /** Start a collapsed log group - for `FileConsumer`, this does the same thing - * as `startGroup`. */ + /** + * Start a collapsed log group - for `FileConsumer`, this does the same thing + * as `startGroup`. + */ override startGroupCollapsed(...args: unknown[]): void { // We don't have a way to collapse groups in the file logger, so we just use the same // function as startGroup. diff --git a/app/ide-desktop/client/src/modules.d.ts b/app/ide-desktop/client/src/modules.d.ts index 0b5d82e9acaa..71fe523d8483 100644 --- a/app/ide-desktop/client/src/modules.d.ts +++ b/app/ide-desktop/client/src/modules.d.ts @@ -1,6 +1,8 @@ -/** @file Type definitions for modules that currently lack typings on DefinitelyTyped. +/** + * @file Type definitions for modules that currently lack typings on DefinitelyTyped. * - * This file MUST NOT `export {}` so that the modules are visible to other files. */ + * This file MUST NOT `export {}` so that the modules are visible to other files. + */ // Required because this is a build artifact, which does not exist on a clean repository. declare module '*/build.json' { diff --git a/app/ide-desktop/client/src/paths.ts b/app/ide-desktop/client/src/paths.ts index 99abdc3ffb43..f44c72743bb6 100644 --- a/app/ide-desktop/client/src/paths.ts +++ b/app/ide-desktop/client/src/paths.ts @@ -11,24 +11,30 @@ import * as paths from '../paths' // === Paths === // ============= -/** The root of the application bundle. +/** + * The root of the application bundle. * * This path is like: * - for packaged application `…/resources/app.asar`; - * - for development `…` (just the directory with `index.js`). */ + * - for development `…` (just the directory with `index.js`). + */ export const APP_PATH = electron.app.getAppPath() -/** The path of the directory in which the log files of IDE are stored. +/** + * The path of the directory in which the log files of IDE are stored. * - * This is based on the Electron `logs` directory, see {@link electron.app.getPath}. */ + * This is based on the Electron `logs` directory, see {@link electron.app.getPath}. + */ export const LOGS_DIRECTORY = electron.app.getPath('logs') /** The application assets, all files bundled with it. */ export const ASSETS_PATH = path.join(APP_PATH, 'assets') -/** Path to the `resources` folder. +/** + * Path to the `resources` folder. * - * Contains other app resources, including binaries, such a project manager. */ + * Contains other app resources, including binaries, such a project manager. + */ export const RESOURCES_PATH = electronIsDev ? APP_PATH : path.join(APP_PATH, '..') /** Project manager binary path. */ diff --git a/app/ide-desktop/client/src/preload.ts b/app/ide-desktop/client/src/preload.ts index 7ddd05ae9597..7243477b82cd 100644 --- a/app/ide-desktop/client/src/preload.ts +++ b/app/ide-desktop/client/src/preload.ts @@ -16,7 +16,7 @@ import type * as projectManagement from '@/projectManagement' // esbuild, we have to manually use "require". Switch this to an import once new electron version // actually honours ".mjs" files for sandboxed preloading (this will likely become an error at that time). // https://www.electronjs.org/fr/docs/latest/tutorial/esm#sandboxed-preload-scripts-cant-use-esm-imports -// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports +// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const electron = require('electron') // ================= diff --git a/app/ide-desktop/client/src/projectManager.ts b/app/ide-desktop/client/src/projectManager.ts index deb942d0bd4a..14305d4023c9 100644 --- a/app/ide-desktop/client/src/projectManager.ts +++ b/app/ide-desktop/client/src/projectManager.ts @@ -17,8 +17,10 @@ const execFile = util.promisify(childProcess.execFile) // === Project Manager === // ======================= -/** Return the Project Manager path. - * @throws If the Project Manager path is invalid. */ +/** + * Return the Project Manager path. + * @throws If the Project Manager path is invalid. + */ export function pathOrPanic(args: config.Args): string { const binPath = args.groups.engine.options.projectManagerPath.value const binExists = fsSync.existsSync(binPath) @@ -35,11 +37,13 @@ async function exec(args: config.Args, processArgs: string[], env?: NodeJS.Proce return await execFile(binPath, processArgs, { env }) } -/** Spawn the Project Manager process. +/** + * Spawn the Project Manager process. * * The standard output and error handles will be redirected to the output and error handles of the * Electron app. Input is piped to this process, so it will not be closed until this process - * finishes. */ + * finishes. + */ export function spawn( args: config.Args, processArgs: string[], diff --git a/app/ide-desktop/client/src/security.ts b/app/ide-desktop/client/src/security.ts index f4e219af95d1..bdb7c2ea144a 100644 --- a/app/ide-desktop/client/src/security.ts +++ b/app/ide-desktop/client/src/security.ts @@ -1,5 +1,7 @@ -/** @file Security configuration of Electron. Most of the options are based on the official security - * guide: https://www.electronjs.org/docs/latest/tutorial/security. */ +/** + * @file Security configuration of Electron. Most of the options are based on the official security + * guide: https://www.electronjs.org/docs/latest/tutorial/security. + */ import * as electron from 'electron' @@ -36,8 +38,10 @@ const WEBVIEW_URL_WHITELIST: string[] = [] // === Utils === // ============= -/** Secure the web preferences of a new window. It deletes potentially unsecure options, making them - * revert to secure defaults. */ +/** + * Secure the web preferences of a new window. It deletes potentially unsecure options, making them + * revert to secure defaults. + */ export function secureWebPreferences(webPreferences: electron.WebPreferences) { delete webPreferences.preload delete webPreferences.nodeIntegration @@ -54,16 +58,20 @@ export function secureWebPreferences(webPreferences: electron.WebPreferences) { // === Security === // ================ -/** Enabling sandbox globally. Follow the link to learn more: - * https://www.electronjs.org/docs/latest/tutorial/sandbox. */ +/** + * Enabling sandbox globally. Follow the link to learn more: + * https://www.electronjs.org/docs/latest/tutorial/sandbox. + */ function enableGlobalSandbox() { electron.app.enableSandbox() } -/** By default, Electron will automatically approve all permission requests unless the developer has +/** + * By default, Electron will automatically approve all permission requests unless the developer has * manually configured a custom handler. While a solid default, security-conscious developers might * want to assume the very opposite. Follow the link to learn more: - * https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content. */ + * https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content. + */ function rejectPermissionRequests() { void electron.app.whenReady().then(() => { electron.session.defaultSession.setPermissionRequestHandler((_webContents, permission) => { @@ -72,7 +80,8 @@ function rejectPermissionRequests() { }) } -/** This Electron app is configured with extra CORS headers. Those headers are added because they +/** + * This Electron app is configured with extra CORS headers. Those headers are added because they * increase security and enable higher resolution for `performance.now()` timers. However, one of * these headers (i.e., `Cross-Origin-Embedder-Policy: require-corp`), breaks the Stripe.js library. * This is because Stripe.js does not provide a `Cross-Origin-Resource-Policy` header or @@ -89,7 +98,8 @@ function rejectPermissionRequests() { * * The missing headers are added more generally to all resources hosted at `https://js.stripe.com`. * This is because the resources are fingerprinted and the fingerprint changes every time the - * resources are updated. Additionally, Stripe.js may choose to add more resources in the future. */ + * resources are updated. Additionally, Stripe.js may choose to add more resources in the future. + */ function addMissingCorsHeaders() { void electron.app.whenReady().then(() => { electron.session.defaultSession.webRequest.onHeadersReceived((details, callback) => { @@ -103,12 +113,14 @@ function addMissingCorsHeaders() { }) } -/** A WebView created in a renderer process that does not have Node.js integration enabled will not +/** + * A WebView created in a renderer process that does not have Node.js integration enabled will not * be able to enable integration itself. However, a WebView will always create an independent * renderer process with its own webPreferences. It is a good idea to control the creation of new * tags from the main process and to verify that their webPreferences do not disable * security features. Follow the link to learn more: - * https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation. */ + * https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation. + */ function limitWebViewCreation() { electron.app.on('web-contents-created', (_event, contents) => { contents.on('will-attach-webview', (event, webPreferences, params) => { @@ -121,10 +133,12 @@ function limitWebViewCreation() { }) } -/** Navigation is a common attack vector. If an attacker can convince your app to navigate away from +/** + * Navigation is a common attack vector. If an attacker can convince your app to navigate away from * its current page, they can possibly force your app to open web sites on the Internet. Follow the * link to learn more: - * https://www.electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation. */ + * https://www.electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation. + */ function preventNavigation() { let lastFocusedWindow = electron.BrowserWindow.getFocusedWindow() electron.app.on('browser-window-focus', () => { @@ -146,11 +160,13 @@ function preventNavigation() { }) } -/** Much like navigation, the creation of new webContents is a common attack vector. Attackers +/** + * Much like navigation, the creation of new webContents is a common attack vector. Attackers * attempt to convince your app to create new windows, frames, or other renderer processes with * more privileges than they had before or with pages opened that they couldn't open before. * Follow the link to learn more: - * https://www.electronjs.org/docs/tutorial/security#13-disable-or-limit-creation-of-new-windows. */ + * https://www.electronjs.org/docs/tutorial/security#13-disable-or-limit-creation-of-new-windows. + */ function disableNewWindowsCreation() { electron.app.on('web-contents-created', (_event, contents) => { contents.setWindowOpenHandler(details => { diff --git a/app/ide-desktop/client/src/server.ts b/app/ide-desktop/client/src/server.ts index 7556ead63852..14c7c183631a 100644 --- a/app/ide-desktop/client/src/server.ts +++ b/app/ide-desktop/client/src/server.ts @@ -75,8 +75,10 @@ export class Config { // === Port Finder === // =================== -/** Determine the initial available communication endpoint, starting from the specified port, - * to provide file hosting services. */ +/** + * Determine the initial available communication endpoint, starting from the specified port, + * to provide file hosting services. + */ async function findPort(port: number): Promise { return await portfinder.getPortPromise({ port, startPort: port, stopPort: port + 4 }) } @@ -85,10 +87,12 @@ async function findPort(port: number): Promise { // === Server === // ============== -/** A simple server implementation. +/** + * A simple server implementation. * * Initially it was based on `union`, but later we migrated to `create-servers`. - * Read this topic to learn why: https://github.com/http-party/http-server/issues/483 */ + * Read this topic to learn why: https://github.com/http-party/http-server/issues/483 + */ export class Server { projectsRootDirectory: string devServer?: vite.ViteDevServer @@ -168,9 +172,11 @@ export class Server { }) } - /** Respond to an incoming request. + /** + * Respond to an incoming request. * @throws {Error} when passing invalid JSON to - * `/api/run-project-manager-command?cli-arguments=`. */ + * `/api/run-project-manager-command?cli-arguments=`. + */ process(request: http.IncomingMessage, response: http.ServerResponse) { const requestUrl = request.url const requestPath = requestUrl?.split('?')[0]?.split('#')[0] diff --git a/app/ide-desktop/client/src/urlAssociations.ts b/app/ide-desktop/client/src/urlAssociations.ts index 43fe0a8a18b5..34e1ff1f90d2 100644 --- a/app/ide-desktop/client/src/urlAssociations.ts +++ b/app/ide-desktop/client/src/urlAssociations.ts @@ -13,13 +13,15 @@ const logger = contentConfig.logger // === Protocol Association === // ============================ -/** Register the application as a handler for our [deep link scheme]{@link common.DEEP_LINK_SCHEME}. +/** + * Register the application as a handler for our [deep link scheme]{@link common.DEEP_LINK_SCHEME}. * * This method is no-op when used under the Electron dev mode, as it requires special handling to * set up the process. * * It is also no-op on macOS, as the OS handles the URL opening by passing the `open-url` event to - * the application, thanks to the information baked in our application by `electron-builder`. */ + * the application, thanks to the information baked in our application by `electron-builder`. + */ export function registerAssociations() { if (!electron.app.isDefaultProtocolClient(common.DEEP_LINK_SCHEME)) { if (electronIsDev) { @@ -40,14 +42,16 @@ export function registerAssociations() { // === URL handling === // ==================== -/** Check if the given list of application startup arguments denotes an attempt to open a URL. +/** + * Check if the given list of application startup arguments denotes an attempt to open a URL. * * For example, this happens on Windows when the browser redirects user using our * [deep link scheme]{@link common.DEEP_LINK_SCHEME}. On macOS this is not used, as the OS * handles the URL opening by passing the `open-url` event to the application. * @param clientArgs - A list of arguments passed to the application, stripped from the initial * executable name and any electron dev mode arguments. - * @returns The URL to open, or `null` if no file was specified. */ + * @returns The URL to open, or `null` if no file was specified. + */ export function argsDenoteUrlOpenAttempt(clientArgs: readonly string[]): URL | null { const arg = clientArgs[0] let result: URL | null = null @@ -69,22 +73,26 @@ export function argsDenoteUrlOpenAttempt(clientArgs: readonly string[]): URL | n let initialUrl: URL | null = null -/** Handle the case where IDE is invoked with a URL to open. +/** + * Handle the case where IDE is invoked with a URL to open. * * This happens on Windows when the browser redirects user using the deep link scheme. - * @param openedUrl - The URL to open. */ + * @param openedUrl - The URL to open. + */ export function handleOpenUrl(openedUrl: URL) { logger.log(`Opening URL '${openedUrl.toString()}'.`) // We must wait for the application to be ready and then send the URL to the renderer process. initialUrl = openedUrl } -/** Register the callback that will be called when the application is requested to open a URL. +/** + * Register the callback that will be called when the application is requested to open a URL. * * This method serves to unify the url handling between macOS and Windows. On macOS, the OS * handles the URL opening by passing the `open-url` event to the application. On Windows, a * new instance of the application is started and the URL is passed as a command line argument. - * @param callback - The callback to call when the application is requested to open a URL. */ + * @param callback - The callback to call when the application is requested to open a URL. + */ export function registerUrlCallback(callback: (url: URL) => void) { if (initialUrl != null) { logger.log(`Got URL from command line: '${initialUrl.toString()}'.`) diff --git a/app/ide-desktop/client/tasks/computeHashes.mjs b/app/ide-desktop/client/tasks/computeHashes.mjs index f0797dfb7ee5..69147dfa3110 100644 --- a/app/ide-desktop/client/tasks/computeHashes.mjs +++ b/app/ide-desktop/client/tasks/computeHashes.mjs @@ -7,22 +7,25 @@ import * as pathModule from 'node:path' // ================= // === Constants === // ================= -/** @typedef {"md5" | "sha1" | "sha256"} ChecksumType */ const CHECKSUM_TYPE = 'sha256' // ================ // === Checksum === // ================ -/** The `type` argument can be one of `md5`, `sha1`, or `sha256`. +/** + * The `type` argument can be one of `md5`, `sha1`, or `sha256`. * @param {string} path - Path to the file. * @param {ChecksumType} type - The checksum algorithm to use. - * @returns {Promise} A promise that resolves to the checksum. */ + * @returns {Promise} A promise that resolves to the checksum. + */ function getChecksum(path, type) { return new Promise( // This JSDoc annotation is required for correct types that are also type-safe. - /** Promise handler resolving to the file's checksum. - * @param {(value: string) => void} resolve - Fulfill the promise with the given value. */ + /** + * Promise handler resolving to the file's checksum. + * @param {(value: string) => void} resolve - Fulfill the promise with the given value. + */ (resolve, reject) => { const hash = cryptoModule.createHash(type) const input = fs.createReadStream(path) @@ -37,18 +40,22 @@ function getChecksum(path, type) { ) } -/** Based on https://stackoverflow.com/a/57371333. +/** + * Based on https://stackoverflow.com/a/57371333. * @param {string} file - The path to the file. * @param {string} extension - The new extension of the file. - * @returns A path with the new exension. */ + * @returns A path with the new exension. + */ function changeExtension(file, extension) { const basename = pathModule.basename(file, pathModule.extname(file)) return pathModule.join(pathModule.dirname(file), `${basename}.${extension}`) } -/** Write the file checksum to the provided path. +/** + * Write the file checksum to the provided path. * @param {string} path - The path to the file. - * @param {ChecksumType} type - The checksum algorithm to use. */ + * @param {ChecksumType} type - The checksum algorithm to use. + */ async function writeFileChecksum(path, type) { const checksum = await getChecksum(path, type) const targetPath = changeExtension(path, type) @@ -60,7 +67,8 @@ async function writeFileChecksum(path, type) { // === Callback === // ================ -/** Generates checksums for all build artifacts. +/** + * Generates checksums for all build artifacts. * @param {import('electron-builder').BuildResult} context - Build information. * @returns {Promise} afterAllArtifactBuild hook result. */ diff --git a/app/ide-desktop/client/tasks/signArchivesMacOs.ts b/app/ide-desktop/client/tasks/signArchivesMacOs.ts index 77f5cda74519..0e6fb9479db0 100644 --- a/app/ide-desktop/client/tasks/signArchivesMacOs.ts +++ b/app/ide-desktop/client/tasks/signArchivesMacOs.ts @@ -1,4 +1,5 @@ -/** @file This script signs the content of all archives that we have for macOS. +/** + * @file This script signs the content of all archives that we have for macOS. * For this to work this needs to run on macOS with `codesign`, and a JDK installed. * `codesign` is needed to sign the files, while the JDK is needed for correct packing * and unpacking of java archives. @@ -11,7 +12,8 @@ * This code is based on https://github.com/electron/electron-osx-sign/pull/231 * but our use-case is unlikely to be supported by `electron-osx-sign` * as it adds a java toolchain as additional dependency. - * This script should be removed once the engine is signed. */ + * This script should be removed once the engine is signed. + */ import * as childProcess from 'node:child_process' import * as fs from 'node:fs/promises' @@ -138,8 +140,10 @@ async function ensoPackageSignables(resourcesDir: string): Promise { /** Information we need to sign a given binary. */ interface SigningContext { - /** A digital identity that is stored in a keychain that is on the calling user's keychain - * search list. We rely on this already being set up by the Electron Builder. */ + /** + * A digital identity that is stored in a keychain that is on the calling user's keychain + * search list. We rely on this already being set up by the Electron Builder. + */ readonly identity: string /** Path to the entitlements file. */ readonly entitlements: string @@ -160,9 +164,11 @@ function run(cmd: string, args: string[], cwd?: string) { return childProcess.execFileSync(cmd, args, { cwd }).toString() } -/** Archive with some binaries that we want to sign. +/** + * Archive with some binaries that we want to sign. * - * Can be either a zip or a jar file. */ + * Can be either a zip or a jar file. + */ class ArchiveToSign implements Signable { /** Looks up for archives to sign using the given path patterns. */ static lookupMany = lookupManyHelper(ArchiveToSign.lookup.bind(this)) @@ -171,8 +177,10 @@ class ArchiveToSign implements Signable { constructor( /** An absolute path to the archive. */ public path: string, - /** A list of patterns for files to sign inside the archive. - * Relative to the root of the archive. */ + /** + * A list of patterns for files to sign inside the archive. + * Relative to the root of the archive. + */ public binaries: glob.Pattern[], ) {} @@ -181,8 +189,10 @@ class ArchiveToSign implements Signable { return lookupHelper(path => new ArchiveToSign(path, binaries))(base, pattern) } - /** Sign content of an archive. This function extracts the archive, signs the required files, - * re-packages the archive and replaces the original. */ + /** + * Sign content of an archive. This function extracts the archive, signs the required files, + * re-packages the archive and replaces the original. + */ async sign(context: SigningContext) { console.log(`Signing archive ${this.path}`) const archiveName = pathModule.basename(this.path) @@ -268,10 +278,12 @@ class BinaryToSign implements Signable { // === Discovering Signables. === // ============================== -/** Helper used to concisely define patterns for an archive to sign. +/** + * Helper used to concisely define patterns for an archive to sign. * * Consists of pattern of the archive path - * and set of patterns for files to sign inside the archive. */ + * and set of patterns for files to sign inside the archive. + */ type ArchivePattern = [glob.Pattern, glob.Pattern[]] /** Like `glob` but returns absolute paths by default. */ @@ -280,8 +292,10 @@ async function globAbsolute(pattern: glob.Pattern, options?: glob.Options): Prom return paths } -/** Glob patterns relative to a given base directory. The base directory is allowed to be a pattern - * as well. */ +/** + * Glob patterns relative to a given base directory. The base directory is allowed to be a pattern + * as well. + */ async function globAbsoluteIn( base: glob.Pattern, pattern: glob.Pattern, diff --git a/app/ide-desktop/client/tests/setup.ts b/app/ide-desktop/client/tests/setup.ts index 448147692225..c836bdb2bcad 100644 --- a/app/ide-desktop/client/tests/setup.ts +++ b/app/ide-desktop/client/tests/setup.ts @@ -18,7 +18,7 @@ export default function setup() { try { fs.accessSync(path, fs.constants.X_OK) return true - } catch (err) { + } catch { return false } }) diff --git a/app/ide-desktop/client/watch.ts b/app/ide-desktop/client/watch.ts index f09c23760306..edad9110486b 100644 --- a/app/ide-desktop/client/watch.ts +++ b/app/ide-desktop/client/watch.ts @@ -1,11 +1,13 @@ -/** @file This script is for watching the whole IDE and spawning the electron process. +/** + * @file This script is for watching the whole IDE and spawning the electron process. * * It sets up watchers for the client and content, and spawns the electron process with the IDE. * The spawned electron process can then use its refresh capability to pull the latest changes * from the watchers. * * If the electron app is closed, the script will restart it, allowing to test the IDE setup. - * To stop, use Ctrl+C. */ + * To stop, use Ctrl+C. + */ import * as childProcess from 'node:child_process' import * as fs from 'node:fs/promises' import * as path from 'node:path' diff --git a/app/ide-desktop/icons/src/index.js b/app/ide-desktop/icons/src/index.js index f3844d0603bc..3c6d16b50f6b 100644 --- a/app/ide-desktop/icons/src/index.js +++ b/app/ide-desktop/icons/src/index.js @@ -6,6 +6,7 @@ import * as fsSync from 'node:fs' import * as fs from 'node:fs/promises' import * as os from 'node:os' import * as path from 'node:path' +import * as process from 'node:process' import * as url from 'node:url' import sharp from 'sharp' diff --git a/app/ydoc-server-polyglot/src/ffiPolyglot.ts b/app/ydoc-server-polyglot/src/ffiPolyglot.ts index 19bbc890dc5f..b820cc3d5894 100644 --- a/app/ydoc-server-polyglot/src/ffiPolyglot.ts +++ b/app/ydoc-server-polyglot/src/ffiPolyglot.ts @@ -2,7 +2,6 @@ * @file This file is used as rust ffi interface for building the polyglot ydoc server. * All the exported methods are provided by the ydoc server implementation. * The interface should be kept in sync with Rust ffi interface {@link ydoc-shared/src/ast/ffi}. - * * @module ffiPolyglot */ diff --git a/app/ydoc-server/src/__tests__/edits.test.ts b/app/ydoc-server/src/__tests__/edits.test.ts index 8ec5eb93fffe..8b41a64cb3bd 100644 --- a/app/ydoc-server/src/__tests__/edits.test.ts +++ b/app/ydoc-server/src/__tests__/edits.test.ts @@ -7,7 +7,8 @@ import { applyDiffAsTextEdits, stupidFastDiff } from '../edits' // === Test utilities === // ====================== -/** Apply text edits intended for language server to a given starting text. Used for verification +/** + * Apply text edits intended for language server to a given starting text. Used for verification * during testing if generated edits were correct. This is intentionally a very simple, not * performant implementation. */ diff --git a/app/ydoc-server/src/auth.ts b/app/ydoc-server/src/auth.ts index fdb2daeb8c36..9b0903a70b1d 100644 --- a/app/ydoc-server/src/auth.ts +++ b/app/ydoc-server/src/auth.ts @@ -10,6 +10,9 @@ export type ConnectionData = { const docNameRegex = /^[a-z0-9/-]+$/i +/** + * Extract the document name from the path name extracted from the connection. + */ export function docName(pathname: string) { const prefix = '/project/' if (pathname != null && pathname.startsWith(prefix)) { diff --git a/app/ydoc-server/src/edits.ts b/app/ydoc-server/src/edits.ts index ef32f656e104..a9a36d2092fe 100644 --- a/app/ydoc-server/src/edits.ts +++ b/app/ydoc-server/src/edits.ts @@ -41,6 +41,9 @@ interface AppliedUpdates { newMetadata: fileFormat.IdeMetadata['node'] | undefined } +/** + * Return an object containing updated versions of relevant fields, given an update payload. + */ export function applyDocumentUpdates( doc: ModuleDoc, synced: EnsoFileParts, @@ -122,6 +125,10 @@ function translateVisualizationToFile( } } +/** + * Convert from the serialized file representation of visualization metadata + * to the internal representation. + */ export function translateVisualizationFromFile( vis: fileFormat.VisualizationMetadata, ): VisualizationMetadata | undefined { @@ -177,11 +184,14 @@ export function stupidFastDiff(oldString: string, newString: string): diff.Diff[ .concat(commonSuffix ? [[0, commonSuffix]] : []) } +/** + * Return a list of text edits describing how to turn one string into another. + */ export function applyDiffAsTextEdits( lineOffset: number, oldString: string, newString: string, -): TextEdit[] { +): readonly TextEdit[] { const changes = oldString.length + newString.length > MAX_SIZE_FOR_NORMAL_DIFF ? stupidFastDiff(oldString, newString) @@ -230,6 +240,9 @@ export function applyDiffAsTextEdits( return edits } +/** + * Pretty print a code diff for display in the terminal using ANSI escapes to control text colors. + */ export function prettyPrintDiff(from: string, to: string): string { const colReset = '\x1b[0m' const colRed = '\x1b[31m' diff --git a/app/ydoc-server/src/fileFormat.ts b/app/ydoc-server/src/fileFormat.ts index 5a5118aaba4f..54c8065b9454 100644 --- a/app/ydoc-server/src/fileFormat.ts +++ b/app/ydoc-server/src/fileFormat.ts @@ -109,18 +109,24 @@ export function tryParseMetadataOrFallback(metadataJson: string | undefined | nu return metadata.parse(parsedMeta) } +/** + * Return a parsed {@link IdMap} from a JSON string, or a default value if parsing failed. + */ export function tryParseIdMapOrFallback(idMapJson: string | undefined | null): IdMap { if (idMapJson == null) return [] const parsedIdMap = tryParseJson(idMapJson) return idMap.parse(parsedIdMap) } +/** + * Parse a JSON string, or return `null` if parsing failed instead of throwing an error. + */ function tryParseJson(jsonString: string) { try { return json.parse(jsonString) - } catch (e) { + } catch (error) { console.error('Failed to parse metadata JSON:') - console.error(e) + console.error(error) return null } } diff --git a/app/ydoc-server/src/index.ts b/app/ydoc-server/src/index.ts index 1d9900a6f7fa..d96704e4f0b5 100644 --- a/app/ydoc-server/src/index.ts +++ b/app/ydoc-server/src/index.ts @@ -33,6 +33,9 @@ export function configureAllDebugLogs( } } +/** + * Create a WebSocket server to host the YDoc coordinating server. + */ export async function createGatewayServer( httpServer: Server | Http2SecureServer, overrideLanguageServerUrl?: string, diff --git a/app/ydoc-server/src/languageServerSession.ts b/app/ydoc-server/src/languageServerSession.ts index 130ebe9c6f5d..e01f3565587d 100644 --- a/app/ydoc-server/src/languageServerSession.ts +++ b/app/ydoc-server/src/languageServerSession.ts @@ -47,6 +47,9 @@ const EXTENSION = '.enso' const debugLog = createDebug('ydoc-server:session') +/** + * + */ export class LanguageServerSession { clientId: Uuid indexDoc: WSSharedDoc @@ -62,6 +65,9 @@ export class LanguageServerSession { static DEBUG = false + /** + * Create a {@link LanguageServerSession}. + */ constructor(url: string) { this.clientScope = new AbortScope() this.clientId = random.uuidv4() as Uuid @@ -89,6 +95,10 @@ export class LanguageServerSession { } static sessions = new Map() + + /** + * Get a {@link LanguageServerSession} by its URL. + */ static get(url: string): LanguageServerSession { const session = map.setIfUndefined( LanguageServerSession.sessions, @@ -201,6 +211,9 @@ export class LanguageServerSession { ) } + /** + * + */ async scanSourceFiles() { this.assertProjectRoot() const sourceDir: Path = { rootId: this.projectRootId, segments: [SOURCE_DIR] } @@ -211,11 +224,17 @@ export class LanguageServerSession { ) } + /** + * + */ tryGetExistingModuleModel(path: Path): ModulePersistence | undefined { const name = pathToModuleName(path) return this.authoritativeModules.get(name) } + /** + * + */ getModuleModel(path: Path): ModulePersistence { const name = pathToModuleName(path) return map.setIfUndefined(this.authoritativeModules, name, () => { @@ -233,10 +252,16 @@ export class LanguageServerSession { }) } + /** + * + */ retain() { this.retainCount += 1 } + /** + * + */ async release(): Promise { this.retainCount -= 1 if (this.retainCount !== 0) return @@ -249,6 +274,9 @@ export class LanguageServerSession { await Promise.all(moduleDisposePromises) } + /** + * Get a YDoc by its id. + */ getYDoc(guid: string): WSSharedDoc | undefined { return this.docs.get(guid) } @@ -335,9 +363,8 @@ class ModulePersistence extends ObservableV2<{ removed: () => void }> { private setState(state: LsSyncState) { if (this.state !== LsSyncState.Disposed) { debugLog('State change: %o -> %o', LsSyncState[this.state], LsSyncState[state]) - // This is SAFE. `this.state` is only `readonly` to ensure that this is the only place - // where it is mutated. - // @ts-expect-error + // @ts-expect-error This is SAFE. `this.state` is only `readonly` to ensure that + // this is the only place where it is mutated. this.state = state if (state === LsSyncState.Synchronized) this.trySyncRemoveUpdates() } else { @@ -346,9 +373,8 @@ class ModulePersistence extends ObservableV2<{ removed: () => void }> { } private setLastAction(lastAction: Promise) { - // This is SAFE. `this.lastAction` is only `readonly` to ensure that this is the only place - // where it is mutated. - // @ts-expect-error + // @ts-expect-error This is SAFE. `this.lastAction` is only `readonly` to ensure that + // this is the only place where it is mutated. this.lastAction = lastAction.then( () => {}, () => {}, @@ -356,8 +382,10 @@ class ModulePersistence extends ObservableV2<{ removed: () => void }> { return lastAction } - /** Set the current state to the given state while the callback is running. - * Set the current state back to {@link LsSyncState.Synchronized} when the callback finishes. */ + /** + * Set the current state to the given state while the callback is running. + * Set the current state back to {@link LsSyncState.Synchronized} when the callback finishes. + */ private async withState(state: LsSyncState, callback: () => void | Promise): Promise private async withState( state: LsSyncState, diff --git a/app/ydoc-server/src/serialization.ts b/app/ydoc-server/src/serialization.ts index f385dfc65166..745ceab109e6 100644 --- a/app/ydoc-server/src/serialization.ts +++ b/app/ydoc-server/src/serialization.ts @@ -4,7 +4,10 @@ import * as json from 'lib0/json' import { ExternalId, IdMap, sourceRangeFromKey } from 'ydoc-shared/yjsModel' import * as fileFormat from './fileFormat' -export function deserializeIdMap(idMapJson: string) { +/** + * Convert a JSON string to an {@link IdMap}. + */ +export function deserializeIdMap(idMapJson: string): IdMap { const idMapMeta = fileFormat.tryParseIdMapOrFallback(idMapJson) const idMap = new IdMap() for (const [{ index, size }, id] of idMapMeta) { @@ -18,12 +21,18 @@ export function deserializeIdMap(idMapJson: string) { return idMap } +/** + * Convert an {@link IdMap} to a JSON string. + */ export function serializeIdMap(map: IdMap): string { map.validate() return json.stringify(idMapToArray(map)) } -export function idMapToArray(map: IdMap): fileFormat.IdMapEntry[] { +/** + * Convert an {@link IdMap} to an array of {@link fileFormat.IdMapEntry}. + */ +export function idMapToArray(map: IdMap): readonly fileFormat.IdMapEntry[] { const entries: fileFormat.IdMapEntry[] = [] map.entries().forEach(([rangeBuffer, id]) => { const decoded = sourceRangeFromKey(rangeBuffer) @@ -37,7 +46,10 @@ export function idMapToArray(map: IdMap): fileFormat.IdMapEntry[] { return entries } -function idMapCmp(a: fileFormat.IdMapEntry, b: fileFormat.IdMapEntry) { +/** + * Compare two {@link fileFormat.IdMapEntry}. + */ +function idMapCmp(a: fileFormat.IdMapEntry, b: fileFormat.IdMapEntry): number { const val1 = a[0]?.index?.value ?? 0 const val2 = b[0]?.index?.value ?? 0 if (val1 === val2) { diff --git a/app/ydoc-server/src/ydoc.ts b/app/ydoc-server/src/ydoc.ts index 87e9fb144197..d4d26c4e20d2 100644 --- a/app/ydoc-server/src/ydoc.ts +++ b/app/ydoc-server/src/ydoc.ts @@ -38,6 +38,9 @@ export class WSSharedDoc { conns: Map> awareness: Awareness + /** + * Create a {@link WSSharedDoc}. + */ constructor(gc = true) { this.doc = new Y.Doc({ gc }) // this.name = name @@ -68,12 +71,18 @@ export class WSSharedDoc { this.doc.on('update', (update, origin) => this.updateHandler(update, origin)) } + /** + * Send a message to all connected clients. + */ broadcast(message: Uint8Array) { for (const [conn] of this.conns) { if (typeof conn !== 'string') conn.send(message) } } + /** + * Process an update event from the YDoc document. + */ updateHandler(update: Uint8Array, _origin: any) { const encoder = encoding.createEncoder() encoding.writeVarUint(encoder, messageSync) @@ -170,7 +179,7 @@ class YjsConnection extends ObservableV2<{ close(): void }> { } try { this.ws.send(message, error => error && this.close()) - } catch (e) { + } catch { this.close() } } @@ -198,10 +207,10 @@ class YjsConnection extends ObservableV2<{ close(): void }> { break } } - } catch (err) { - console.error(err) - // @ts-ignore - this.wsDoc.doc.emit('error', [err]) + } catch (error) { + console.error(error) + // @ts-expect-error Emit a custom event. + this.wsDoc.doc.emit('error', [error]) } } @@ -214,7 +223,7 @@ class YjsConnection extends ObservableV2<{ close(): void }> { this.ws.close() this.emit('close', []) if (this.wsDoc.conns.size === 0) { - // @ts-ignore + // @ts-expect-error Emit a custom event. this.wsDoc.doc.emit('unload', []) } } diff --git a/app/ydoc-shared/parser-codegen/codegen.ts b/app/ydoc-shared/parser-codegen/codegen.ts index 77f00ee3ea53..4497e64dbd9d 100644 --- a/app/ydoc-shared/parser-codegen/codegen.ts +++ b/app/ydoc-shared/parser-codegen/codegen.ts @@ -35,6 +35,9 @@ const viewIdent = tsf.createIdentifier('view') // === Public API === +/** + * Generate TypeScript code that implements a given schema. + */ export function implement(schema: Schema.Schema): string { const file = ts.createSourceFile('source.ts', '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS) const printer = ts.createPrinter({ diff --git a/app/ydoc-shared/parser-codegen/index.ts b/app/ydoc-shared/parser-codegen/index.ts index f9f2daee51f8..3411988489aa 100644 --- a/app/ydoc-shared/parser-codegen/index.ts +++ b/app/ydoc-shared/parser-codegen/index.ts @@ -27,7 +27,7 @@ function usage(): never { } const schemaContents = await fs.readFile(schemaPath, 'utf8') -var hash = crypto.createHash('sha1') +const hash = crypto.createHash('sha1') hash.update(schemaContents) const schemaHash = hash.digest() @@ -45,7 +45,7 @@ const checkSchemaChanged = async () => { try { const lastDigest = await fs.readFile(`${outputPath}.schema-digest`) return !lastDigest.equals(schemaHash) - } catch (e) { + } catch { return true } } diff --git a/app/ydoc-shared/parser-codegen/serialization.ts b/app/ydoc-shared/parser-codegen/serialization.ts index 78aaff9b617b..c377ed1b7d6d 100644 --- a/app/ydoc-shared/parser-codegen/serialization.ts +++ b/app/ydoc-shared/parser-codegen/serialization.ts @@ -77,6 +77,9 @@ type VisitorApplicator = (cursor: ts.Expression, offset: AccessOffset) => ts.Exp // === Public API === +/** + * + */ export class Type { readonly type: ts.TypeNode readonly reader: ReadApplicator @@ -95,16 +98,25 @@ export class Type { this.size = size } + /** + * + */ static Abstract(name: string): Type { const valueReader = callRead(name) return new Type(tsf.createTypeReferenceNode(name), valueReader, 'visitValue', POINTER_SIZE) } + /** + * + */ static Concrete(name: string, size: number): Type { const valueReader = callRead(name) return new Type(tsf.createTypeReferenceNode(name), valueReader, 'visitValue', size) } + /** + * + */ static Sequence(element: Type): Type { return new Type( tsf.createTypeReferenceNode('IterableIterator', [element.type]), @@ -114,6 +126,9 @@ export class Type { ) } + /** + * + */ static Option(element: Type): Type { return new Type( tsf.createUnionTypeNode([element.type, noneType]), @@ -123,6 +138,9 @@ export class Type { ) } + /** + * + */ static Result(ok: Type, err: Type): Type { return new Type( support.Result(ok.type, err.type), @@ -179,6 +197,9 @@ export class Type { ) } +/** + * + */ export function seekView(view: ts.Expression, address: number): ts.Expression { if (address === 0) { return view @@ -187,10 +208,16 @@ export function seekView(view: ts.Expression, address: number): ts.Expression { } } +/** + * + */ export function seekViewDyn(view: ts.Expression, address: ts.Expression): ts.Expression { return tsf.createCallExpression(support.readOffset, [], [view, address]) } +/** + * + */ export function abstractTypeVariants(cases: ts.Identifier[]): ts.Statement { const reads = cases.map(c => tsf.createPropertyAccessChain(c, undefined, 'read')) return tsf.createVariableStatement( @@ -209,6 +236,9 @@ export function abstractTypeVariants(cases: ts.Identifier[]): ts.Statement { ) } +/** + * + */ export function abstractTypeDeserializer( ident: ts.Identifier, cursorIdent: ts.Identifier, @@ -221,6 +251,9 @@ export function abstractTypeDeserializer( ) } +/** + * + */ export function fieldDeserializer( ident: ts.Identifier, type: Type, @@ -239,6 +272,9 @@ export function fieldDeserializer( ) } +/** + * + */ export function fieldVisitor( ident: ts.Identifier, type: Type, @@ -273,8 +309,10 @@ function thisAccess(ident: ts.Identifier): ts.PropertyAccessExpression { // === Implementation === -/** Returns a function that, given an expression evaluating to a [`Cursor`], returns an expression applying a - * deserialization method with the given name to the cursor. */ +/** + * Returns a function that, given an expression evaluating to a [`Cursor`], returns an expression applying a + * deserialization method with the given name to the cursor. + */ function primitiveReader(func: ts.Identifier): ReadApplicator { return (view, address) => tsf.createCallExpression(func, [], [view, materializeAddress(address)]) } @@ -322,8 +360,10 @@ function materializeAddress(offset: AccessOffset): ts.Expression { } } -/** Similar to [`readerTransformer`], but for deserialization-transformers that produce a reader by combining two input - * readers. */ +/** + * Similar to [`readerTransformer`], but for deserialization-transformers that produce a reader by combining two input + * readers. + */ function readerTransformerTwoTyped( func: ts.Identifier, ): (readOk: ReadApplicator, readErr: ReadApplicator) => ReadApplicator { @@ -336,6 +376,9 @@ function readerTransformerTwoTyped( } } +/** + * + */ export function callRead(ident: string): ReadApplicator { return (view, address) => tsf.createCallExpression( @@ -345,6 +388,9 @@ export function callRead(ident: string): ReadApplicator { ) } +/** + * + */ export function createSequenceReader(size: number, reader: ReadApplicator): ReadApplicator { const sizeLiteral = tsf.createNumericLiteral(size) const closure = readerClosure(reader) @@ -390,6 +436,9 @@ function createResultVisitor( ) } +/** + * + */ export function visitorClosure( visitor: VisitorApplicator | 'visitValue' | undefined, reader: ReadApplicator, @@ -407,6 +456,9 @@ export function visitorClosure( } } +/** + * + */ export function readerClosure(reader: ReadApplicator): ts.Expression { const view = tsf.createIdentifier('view') const address = tsf.createIdentifier('address') diff --git a/app/ydoc-shared/parser-codegen/util.ts b/app/ydoc-shared/parser-codegen/util.ts index f5753295f9d4..36950715b036 100644 --- a/app/ydoc-shared/parser-codegen/util.ts +++ b/app/ydoc-shared/parser-codegen/util.ts @@ -4,11 +4,17 @@ const tsf = ts.factory // === Identifier utilities === +/** + * Convert an identifier from an arbitrary case into PascalCase. + */ export function toPascal(ident: string): string { if (ident.includes('.')) throw new Error('toPascal cannot be applied to a namespaced name.') return changeCase.pascalCase(ident) } +/** + * Convert an identifier from an arbitrary case into camelCase. + */ export function toCamel(ident: string): string { if (ident.includes('.')) throw new Error('toCamel cannot be applied to a namespaced name.') return changeCase.camelCase(ident) @@ -30,10 +36,16 @@ const RENAME = new Map([ ['codeStartUtf16', 'startInCodeBuffer'], ]) +/** + * Rename certain special-cased identifiers to avoid using language keywords, and for increased clarity. + */ export function mapIdent(ident: string): string { return RENAME.get(ident) ?? ident } +/** + * Return a name with an optional namespace, normalized to PascalCase. + */ export function namespacedName(name: string, namespace?: string): string { if (namespace == null) { return toPascal(name) @@ -53,12 +65,19 @@ export const modifiers = { protected: tsf.createModifier(ts.SyntaxKind.ProtectedKeyword), } as const +/** + * Create a TypeScript assignment statement. + */ export function assignmentStatement(left: ts.Expression, right: ts.Expression): ts.Statement { return tsf.createExpressionStatement( tsf.createBinaryExpression(left, ts.SyntaxKind.EqualsToken, right), ) } +/** + * Create a TypeScript `class` constructor that forwards a single parameter to its parent class' + * constructor. + */ export function forwardToSuper( ident: ts.Identifier, type: ts.TypeNode, @@ -75,16 +94,26 @@ export function forwardToSuper( ) } +/** + * Create a TypeScript `switch` statement with an additional `default` case that throws an error + * with the given message. + */ export function casesOrThrow(cases: ts.CaseClause[], error: string): ts.CaseBlock { return tsf.createCaseBlock([...cases, tsf.createDefaultClause([throwError(error)])]) } +/** + * Create a TypeScript `throw` statement. + */ export function throwError(error: string): ts.Statement { return tsf.createThrowStatement( tsf.createNewExpression(tsf.createIdentifier('Error'), [], [tsf.createStringLiteral(error)]), ) } +/** + * Create a TypeScript `=>` function with the given single expression as its body. + */ export function makeArrow(params: ts.BindingName[], expr: ts.Expression) { return tsf.createArrowFunction( [], diff --git a/app/ydoc-shared/src/ast/debug.ts b/app/ydoc-shared/src/ast/debug.ts index 15d0c6388fcc..a15dba2f217f 100644 --- a/app/ydoc-shared/src/ast/debug.ts +++ b/app/ydoc-shared/src/ast/debug.ts @@ -1,6 +1,8 @@ import { Ast } from './tree' -/// Returns a GraphViz graph illustrating parent/child relationships in the given subtree. +/** + * Returns a GraphViz graph illustrating parent/child relationships in the given subtree. + */ export function graphParentPointers(ast: Ast) { const sanitize = (id: string) => id.replace('ast:', '').replace(/[^A-Za-z0-9]/g, '') const parentToChild = new Array<{ parent: string; child: string }>() diff --git a/app/ydoc-shared/src/ast/ffi.ts b/app/ydoc-shared/src/ast/ffi.ts index 32d4a638860e..f72e89702f3d 100644 --- a/app/ydoc-shared/src/ast/ffi.ts +++ b/app/ydoc-shared/src/ast/ffi.ts @@ -1,6 +1,5 @@ /** * @file Provides the Rust ffi interface. The interface should be kept in sync with polyglot ffi inteface {@link module:ffiPolyglot}. - * * @module ffi */ @@ -9,6 +8,9 @@ import type { IDataType } from 'hash-wasm/dist/lib/util' import { is_ident_or_operator, is_numeric_literal, parse, parse_doc_to_json } from 'rust-ffi' const xxHasher128 = await createXXHash128() +/** + * Return the xxhash hash for the given buffer. + */ export function xxHash128(input: IDataType) { xxHasher128.init() xxHasher128.update(input) diff --git a/app/ydoc-shared/src/ast/index.ts b/app/ydoc-shared/src/ast/index.ts index 956f78239398..25baecb62256 100644 --- a/app/ydoc-shared/src/ast/index.ts +++ b/app/ydoc-shared/src/ast/index.ts @@ -13,7 +13,8 @@ export * from './token' export * from './tree' declare const brandOwned: unique symbol -/** Used to mark references required to be unique. +/** + * Used to mark references required to be unique. * * Note that the typesystem cannot stop you from copying an `Owned`, * but that is an easy mistake to see (because it occurs locally). @@ -30,6 +31,9 @@ export function asOwned(t: T): Owned { export type NodeChild = { whitespace: string | undefined; node: T } export type RawNodeChild = NodeChild | NodeChild +/** + * Create a new random {@link ExternalId}. + */ export function newExternalId(): ExternalId { return random.uuidv4() as ExternalId } @@ -72,7 +76,8 @@ function unwrapGroups(ast: Ast) { return ast } -/** Tries to recognize inputs that are semantically-equivalent to a sequence of `App`s, and returns the arguments +/** + * Tries to recognize inputs that are semantically-equivalent to a sequence of `App`s, and returns the arguments * identified and LHS of the analyzable chain. * * In particular, this function currently recognizes syntax used in visualization-preprocessor expressions. diff --git a/app/ydoc-shared/src/ast/mutableModule.ts b/app/ydoc-shared/src/ast/mutableModule.ts index 89b278b36a08..7102e1d1a085 100644 --- a/app/ydoc-shared/src/ast/mutableModule.ts +++ b/app/ydoc-shared/src/ast/mutableModule.ts @@ -49,6 +49,9 @@ type YNodes = Y.Map type UpdateObserver = (update: ModuleUpdate) => void type YjsObserver = (events: Y.YEvent[], transaction: Y.Transaction) => void +/** + * + */ export class MutableModule implements Module { private readonly nodes: YNodes private updateObservers: UpdateObserver[] | undefined @@ -66,24 +69,39 @@ export class MutableModule implements Module { return instance as Mutable } + /** + * + */ edit(): MutableModule { const doc = new Y.Doc() Y.applyUpdateV2(doc, Y.encodeStateAsUpdateV2(this.ydoc)) return new MutableModule(doc) } + /** + * + */ applyEdit(edit: MutableModule, origin: Origin = defaultLocalOrigin) { Y.applyUpdateV2(this.ydoc, Y.encodeStateAsUpdateV2(edit.ydoc), origin) } + /** + * + */ transact(f: () => T, origin: Origin = defaultLocalOrigin): T { return this.ydoc.transact(f, origin) } + /** + * + */ root(): MutableAst | undefined { return this.rootPointer()?.expression } + /** + * + */ replaceRoot(newRoot: Owned | undefined): Owned | undefined { if (newRoot) { const rootPointer = this.rootPointer() @@ -105,11 +123,17 @@ export class MutableModule implements Module { } } + /** + * + */ syncRoot(root: Owned) { this.replaceRoot(root) this.gc() } + /** + * + */ syncToCode(code: string) { const root = this.root() if (root) { @@ -167,10 +191,16 @@ export class MutableModule implements Module { return materializeMutable(this, fields) as Owned> } + /** + * + */ static Transient() { return new this(new Y.Doc()) } + /** + * + */ observe(observer: (update: ModuleUpdate) => void) { this.updateObservers ??= [] this.updateObservers.push(observer) @@ -190,6 +220,9 @@ export class MutableModule implements Module { this.nodes.observeDeep(this.yjsObserver) } + /** + * + */ unobserve(handle: UpdateObserver) { const i = this.updateObservers?.indexOf(handle) if (i == null || i < 0) return @@ -204,6 +237,9 @@ export class MutableModule implements Module { return !!this.updateObservers?.length } + /** + * + */ getStateAsUpdate(): ModuleUpdate { const updateBuilder = new UpdateBuilder(this, this.nodes, undefined) for (const id of this.nodes.keys()) { @@ -213,6 +249,9 @@ export class MutableModule implements Module { return updateBuilder.finish() } + /** + * + */ applyUpdate(update: Uint8Array, origin: Origin): ModuleUpdate | undefined { let summary: ModuleUpdate | undefined const observer = (events: Y.YEvent[]) => { @@ -272,12 +311,24 @@ export class MutableModule implements Module { return updateBuilder.finish() } + /** + * + */ clear() { this.nodes.clear() } + /** + * + */ get(id: AstId): Mutable + /** + * + */ get(id: AstId | undefined): Mutable | undefined + /** + * + */ get(id: AstId | undefined): Mutable | undefined { if (!id) return undefined const ast = this.tryGet(id) @@ -285,6 +336,9 @@ export class MutableModule implements Module { return ast } + /** + * + */ tryGet(id: AstId | undefined): Mutable | undefined { if (!id) return undefined const nodeData = this.nodes.get(id) @@ -293,24 +347,39 @@ export class MutableModule implements Module { return materializeMutable(this, fields) } + /** + * + */ replace(id: AstId, value: Owned): Owned | undefined { return this.tryGet(id)?.replace(value) } + /** + * + */ replaceValue(id: AstId, value: Owned): Owned | undefined { return this.tryGet(id)?.replaceValue(value) } + /** + * + */ take(id: AstId): Owned { return this.replace(id, Wildcard.new(this)) || asOwned(this.get(id)) } + /** + * + */ updateValue(id: AstId, f: (x: Owned) => Owned): T | undefined { return this.tryGet(id)?.updateValue(f) } ///////////////////////////////////////////// + /** + * + */ constructor(doc: Y.Doc) { this.nodes = doc.getMap('nodes') } @@ -323,9 +392,9 @@ export class MutableModule implements Module { /** @internal */ baseObject(type: string, externalId?: ExternalId, overrideId?: AstId): FixedMap { const map = new Y.Map() - const map_ = map as unknown as FixedMap<{}> + const map_ = map as unknown as FixedMap const id = overrideId ?? newAstId(type) - const metadata = new Y.Map() as unknown as FixedMap<{}> + const metadata = new Y.Map() as unknown as FixedMap const metadataFields = setAll(metadata, { externalId: externalId ?? newExternalId(), }) @@ -342,25 +411,40 @@ export class MutableModule implements Module { /** @internal */ getToken(token: SyncTokenId): Token + /** + * + */ getToken(token: SyncTokenId | undefined): Token | undefined + /** + * + */ getToken(token: SyncTokenId | undefined): Token | undefined { if (!token) return token if (token instanceof Token) return token return Token.withId(token.code_, token.tokenType_, token.id) } + /** + * + */ getAny(node: AstId | SyncTokenId): MutableAst | Token { return isTokenId(node) ? this.getToken(node) : this.get(node) } + /** + * + */ getConcrete(child: RawNodeChild): NodeChild | NodeChild { if (isTokenId(child.node)) return { whitespace: child.whitespace, node: this.getToken(child.node) } else return { whitespace: child.whitespace, node: this.get(child.node) } } - /** @internal Copy a node into the module, if it is bound to a different module. */ + /** @internal */ copyIfForeign(ast: Owned): Owned + /** + * + */ copyIfForeign(ast: Owned | undefined): Owned | undefined { if (!ast) return ast if (ast.module === this) return ast @@ -390,10 +474,16 @@ export const __TEST = { newAstId } /** Checks whether the input looks like an AstId. */ const astIdRegex = /^ast:[A-Za-z]+#[0-9]+$/ +/** + * + */ export function isAstId(value: string): value is AstId { return astIdRegex.test(value) } +/** + * + */ export function assertAstId(value: string): asserts value is AstId { assert(isAstId(value), `Incorrect AST ID: ${value}`) } diff --git a/app/ydoc-shared/src/ast/parse.ts b/app/ydoc-shared/src/ast/parse.ts index a17d294718f5..09b2aecdd5c9 100644 --- a/app/ydoc-shared/src/ast/parse.ts +++ b/app/ydoc-shared/src/ast/parse.ts @@ -96,6 +96,9 @@ export function abstract( code: string, substitutor?: (key: NodeKey) => Owned | undefined, ): { root: Owned; spans: SpanMap; toRaw: Map } +/** + * Implementation of `abstract`. + */ export function abstract( module: MutableModule, tree: RawAst.Tree, @@ -469,7 +472,7 @@ export function print(ast: Ast): PrintedSource { return { info, code } } -/** @internal Used by `Ast.printSubtree`. Note that some AST types have overrides. */ +/** @internal */ export function printAst( ast: Ast, info: SpanMap, @@ -511,7 +514,7 @@ export function printAst( return code } -/** @internal Use `Ast.code()' to stringify. */ +/** @internal */ export function printBlock( block: BodyBlock, info: SpanMap, @@ -553,7 +556,7 @@ export function printBlock( return code } -/** @internal Use `Ast.code()' to stringify. */ +/** @internal */ export function printDocumented( documented: Documented, info: SpanMap, @@ -623,7 +626,8 @@ export function parseBlockWithSpans( return abstract(module, tree, code) } -/** Parse the input, and apply the given `IdMap`. Return the parsed tree, the updated `IdMap`, the span map, and a +/** + * Parse the input, and apply the given `IdMap`. Return the parsed tree, the updated `IdMap`, the span map, and a * mapping to the `RawAst` representation. */ export function parseExtended(code: string, idMap?: IdMap | undefined, inModule?: MutableModule) { @@ -649,7 +653,8 @@ export function astCount(ast: Ast): number { return count } -/** Apply an `IdMap` to a module, using the given `SpanMap`. +/** + * Apply an `IdMap` to a module, using the given `SpanMap`. * @returns The number of IDs that were assigned from the map. */ export function setExternalIds(edit: MutableModule, spans: SpanMap, ids: IdMap): number { @@ -667,7 +672,8 @@ export function setExternalIds(edit: MutableModule, spans: SpanMap, ids: IdMap): return astsMatched } -/** Try to find all the spans in `expected` in `encountered`. If any are missing, use the provided `code` to determine +/** + * Try to find all the spans in `expected` in `encountered`. If any are missing, use the provided `code` to determine * whether the lost spans are single-line or multi-line. */ function checkSpans(expected: NodeSpanMap, encountered: NodeSpanMap, code: string) { @@ -690,7 +696,8 @@ function checkSpans(expected: NodeSpanMap, encountered: NodeSpanMap, code: strin return { lostInline, lostBlock } } -/** If the input tree's concrete syntax has precedence errors (i.e. its expected code would not parse back to the same +/** + * If the input tree's concrete syntax has precedence errors (i.e. its expected code would not parse back to the same * structure), try to fix it. If possible, it will be repaired by inserting parentheses; if that doesn't fix it, the * affected subtree will be re-synced to faithfully represent the source code the incorrect tree prints to. */ @@ -741,7 +748,6 @@ export function repair( /** * Replace subtrees in the module to ensure that the module contents are consistent with the module's code. - * * @param badAsts - ASTs that, if printed, would not parse to exactly their current content. * @param badSpans - Span map produced by printing the `badAsts` nodes and all their parents. * @param goodSpans - Span map produced by parsing the code from the module of `badAsts`. @@ -783,7 +789,7 @@ function resync( ) } -/** @internal Recursion helper for {@link syntaxHash}. */ +/** @internal */ function hashSubtreeSyntax(ast: Ast, hashesOut: Map): SyntaxHash { let content = '' content += ast.typeName + ':' @@ -808,7 +814,8 @@ function hashString(input: string): SyntaxHash { return xxHash128(input) as SyntaxHash } -/** Calculates `SyntaxHash`es for the given node and all its children. +/** + * Calculates `SyntaxHash`es for the given node and all its children. * * Each `SyntaxHash` summarizes the syntactic content of an AST. If two ASTs have the same code and were parsed the * same way (i.e. one was not parsed in a context that resulted in a different interpretation), they will have the same @@ -860,7 +867,7 @@ function calculateCorrespondence( ) const partAfterToAstBefore = new Map() for (const [spanBefore, partAfter] of spansBeforeAndAfter) { - const astBefore = astSpans.get(sourceRangeKey(spanBefore) as NodeKey)?.[0]! + const astBefore = astSpans.get(sourceRangeKey(spanBefore) as NodeKey)![0]! partAfterToAstBefore.set(sourceRangeKey(partAfter), astBefore) } const matchingPartsAfter = spansBeforeAndAfter.map(([_before, after]) => after) diff --git a/app/ydoc-shared/src/ast/parserSupport.ts b/app/ydoc-shared/src/ast/parserSupport.ts index caad26845a52..cb7ba103ad76 100644 --- a/app/ydoc-shared/src/ast/parserSupport.ts +++ b/app/ydoc-shared/src/ast/parserSupport.ts @@ -16,10 +16,16 @@ export abstract class LazyObject { this._v = view } + /** + * + */ visitChildren(_visitor: ObjectVisitor): boolean { return false } + /** + * + */ children(): LazyObject[] { const children: LazyObject[] = [] this.visitChildren(child => { @@ -35,40 +41,67 @@ function makeDataView(buffer: ArrayBuffer, address: number) { return new DataView(buffer, address) } +/** + * + */ export function readU8(view: DataView, address: number) { return view.getUint8(address) } +/** + * + */ export function readU32(view: DataView, address: number) { return view.getUint32(address, true) } +/** + * + */ export function readI32(view: DataView, address: number) { return view.getInt32(address, true) } +/** + * + */ export function readU64(view: DataView, address: number) { return view.getBigUint64(address, true) } +/** + * + */ export function readI64(view: DataView, address: number) { return view.getBigInt64(address, true) } +/** + * + */ export function readBool(view: DataView, address: number) { return readU8(view, address) !== 0 } +/** + * + */ export function readOffset(view: DataView, offset: number) { return makeDataView(view.buffer, view.byteOffset + offset) } +/** + * + */ export function readPointer(view: DataView, address: number): DataView { return makeDataView(view.buffer, readU32(view, address)) } const textDecoder = new TextDecoder() +/** + * + */ export function readOption( view: DataView, address: number, @@ -81,6 +114,9 @@ export function readOption( return result } +/** + * + */ export function visitOption( view: DataView, address: number, @@ -97,6 +133,9 @@ export function visitOption( } } +/** + * + */ export function readResult( view: DataView, address: number, @@ -115,6 +154,9 @@ export function readResult( } } +/** + * + */ export function visitResult( view: DataView, address: number, @@ -135,6 +177,9 @@ export function visitResult( } } +/** + * + */ export function visitSequence( view: DataView, address: number, @@ -151,6 +196,9 @@ export function visitSequence( return false } +/** + * + */ export function readSequence( view: DataView, address: number, @@ -163,12 +211,18 @@ export function readSequence( return new LazySequence(offset, size, end, (offset: number) => reader(data, offset)) } +/** + * + */ export class LazySequence implements IterableIterator { private offset: number private readonly step: number private readonly end: number private readonly read: (address: number) => T + /** + * + */ constructor(offset: number, step: number, end: number, read: (address: number) => T) { this.read = read this.offset = offset @@ -176,10 +230,16 @@ export class LazySequence implements IterableIterator { this.end = end } + /** + * + */ [Symbol.iterator]() { return this } + /** + * + */ public next(): IteratorResult { if (this.offset >= this.end) { return { done: true, value: undefined } @@ -190,6 +250,9 @@ export class LazySequence implements IterableIterator { } } +/** + * + */ export function readString(view: DataView, address: number): string { const data = readPointer(view, address) const len = readU32(data, 0) @@ -197,6 +260,9 @@ export function readString(view: DataView, address: number): string { return textDecoder.decode(bytes) } +/** + * + */ export function readEnum(readers: Reader[], view: DataView, address: number): T { const data = readPointer(view, address) const discriminant = readU32(data, 0) diff --git a/app/ydoc-shared/src/ast/sourceDocument.ts b/app/ydoc-shared/src/ast/sourceDocument.ts index ea1b172b8cfc..9b6d93e6769e 100644 --- a/app/ydoc-shared/src/ast/sourceDocument.ts +++ b/app/ydoc-shared/src/ast/sourceDocument.ts @@ -5,7 +5,8 @@ import { offsetEdit, textChangeToEdits } from '../util/data/text' import type { Origin, SourceRange } from '../yjsModel' import { rangeEquals, sourceRangeFromKey } from '../yjsModel' -/** Provides a view of the text representation of a module, +/** + * Provides a view of the text representation of a module, * and information about the correspondence between the text and the ASTs, * that can be kept up-to-date by applying AST changes. */ @@ -20,10 +21,16 @@ export class SourceDocument { this.observers = [] } + /** + * Create an empty {@link SourceDocument}. + */ static Empty() { return new this('', new Map()) } + /** + * Reset this {@link SourceDocument} to an empty state. + */ clear() { if (this.spans.size !== 0) this.spans.clear() if (this.text_ !== '') { @@ -33,6 +40,9 @@ export class SourceDocument { } } + /** + * Apply a {@link ModuleUpdate} and notify observers of the edits. + */ applyUpdate(module: Module, update: ModuleUpdate) { for (const id of update.nodesDeleted) this.spans.delete(id) const root = module.root() @@ -65,30 +75,42 @@ export class SourceDocument { } } + /** + * Get the entire text representation of this module. + */ get text(): string { return this.text_ } + /** + * Get a span in this document by its {@link AstId}. + */ getSpan(id: AstId): SourceRange | undefined { return this.spans.get(id) } + /** + * Add a callback to be called with a list of edits on every update. + */ observe(observer: SourceDocumentObserver) { this.observers.push(observer) if (this.text_.length) observer([{ range: [0, 0], insert: this.text_ }], undefined) } + /** + * Remove a callback to no longer be called with a list of edits on every update. + */ unobserve(observer: SourceDocumentObserver) { const index = this.observers.indexOf(observer) if (index !== undefined) this.observers.splice(index, 1) } - private notifyObservers(textEdits: SourceRangeEdit[], origin: Origin | undefined) { + private notifyObservers(textEdits: readonly SourceRangeEdit[], origin: Origin | undefined) { for (const o of this.observers) o(textEdits, origin) } } export type SourceDocumentObserver = ( - textEdits: SourceRangeEdit[], + textEdits: readonly SourceRangeEdit[], origin: Origin | undefined, ) => void diff --git a/app/ydoc-shared/src/ast/text.ts b/app/ydoc-shared/src/ast/text.ts index cfdb87a440a8..f09cbdeee4ed 100644 --- a/app/ydoc-shared/src/ast/text.ts +++ b/app/ydoc-shared/src/ast/text.ts @@ -52,7 +52,7 @@ function escapeChar(char: string) { /** * Escape a string so it can be safely spliced into an interpolated (`''`) Enso string. * Note: Escape sequences are NOT interpreted in raw (`""`) string literals. - * */ + */ export function escapeTextLiteral(rawString: string) { return rawString.replace(escapeRegex, escapeChar) } diff --git a/app/ydoc-shared/src/ast/token.ts b/app/ydoc-shared/src/ast/token.ts index ecfbd2183a11..50742313fd70 100644 --- a/app/ydoc-shared/src/ast/token.ts +++ b/app/ydoc-shared/src/ast/token.ts @@ -6,10 +6,16 @@ import { isUuid } from '../yjsModel' import { is_ident_or_operator } from './ffi' import * as RawAst from './generated/ast' -export function isToken(t: unknown): t is Token { - return t instanceof Token +/** + * Whether the given value is a {@link Token}. + */ +export function isToken(maybeToken: unknown): maybeToken is Token { + return maybeToken instanceof Token } +/** + * Whether the given {@link NodeChild} is a {@link NodeChild}<{@link Token}>. + */ export function isTokenChild(child: NodeChild): child is NodeChild { return isToken(child.node) } @@ -28,6 +34,9 @@ export interface SyncTokenId { readonly tokenType_: RawAst.Token.Type | undefined } +/** + * A structure representing a lexical source code unit in the AST. + */ export class Token implements SyncTokenId { readonly id: TokenId code_: string @@ -39,27 +48,45 @@ export class Token implements SyncTokenId { this.tokenType_ = type } + /** + * The id of this token. + */ get externalId(): TokenId { return this.id } + /** + * Construct a {@link Token} without a {@link TokenId}. + */ static new(code: string, type?: RawAst.Token.Type) { return new this(code, type, newTokenId()) } + /** + * Construct a {@link Token} with a {@link TokenId}. + */ static withId(code: string, type: RawAst.Token.Type | undefined, id: TokenId) { assert(isUuid(id)) return new this(code, type, id) } + /** + * Whether one {@link SyncTokenId} is equal to another. + */ static equal(a: SyncTokenId, b: SyncTokenId): boolean { return a.tokenType_ === b.tokenType_ && a.code_ === b.code_ } + /** + * The code represented by this token. + */ code(): string { return this.code_ } + /** + * The name of the token type of this token. + */ typeName(): string { if (this.tokenType_) return RawAst.Token.typeNames[this.tokenType_]! else return 'Raw' @@ -79,7 +106,8 @@ declare const identifierBrand: unique symbol declare const typeOrConsIdentifierBrand: unique symbol declare const operatorBrand: unique symbol -/** A string representing a valid qualified name of our language. +/** + * A string representing a valid qualified name of our language. * * In our language, the segments are separated by `.`. All the segments except the last must be lexical identifiers. The * last may be an identifier or a lexical operator. A single identifier is also a valid qualified name. @@ -95,7 +123,8 @@ export type TypeOrConstructorIdentifier = Identifier & { [typeOrConsIdentifierBr /** A string representing a lexical operator. */ export type Operator = string & { [operatorBrand]: never; [qualifiedNameBrand]: never } -/** A string that can be parsed as an identifier in some contexts. +/** + * A string that can be parsed as an identifier in some contexts. * * If it is lexically an identifier (see `StrictIdentifier`), it can be used as identifier anywhere. * @@ -105,7 +134,8 @@ export type Operator = string & { [operatorBrand]: never; [qualifiedNameBrand]: */ export type IdentifierOrOperatorIdentifier = Identifier | Operator -/** Returns true if `code` can be used as an identifier in some contexts. +/** + * Returns true if `code` can be used as an identifier in some contexts. * * If it is lexically an identifier (see `isIdentifier`), it can be used as identifier anywhere. * @@ -119,21 +149,28 @@ export function isIdentifierOrOperatorIdentifier( return is_ident_or_operator(code) !== 0 } -/** Returns true if `code` is lexically an identifier. */ +/** Whether the given code is lexically an identifier. */ export function isIdentifier(code: string): code is Identifier { return is_ident_or_operator(code) === 1 } +/** + * Whether the given code is a type or constructor identifier. + * This is true if the code is an identifier beginning with an uppercase letter. + */ export function isTypeOrConsIdentifier(code: string): code is TypeOrConstructorIdentifier { const isUppercase = (s: string) => s.toUpperCase() === s && s.toLowerCase() !== s return isIdentifier(code) && code.length > 0 && isUppercase(code[0]!) } +/** + * The code as an {@link Identifier} if it is an {@link Identifier}, else `undefined`. + */ export function identifier(code: string): Identifier | undefined { if (isIdentifier(code)) return code } -/** Returns true if `code` is lexically an operator. */ +/** Whether the given code is lexically an operator. */ export function isOperator(code: string): code is Operator { return is_ident_or_operator(code) === 2 } diff --git a/app/ydoc-shared/src/ast/tree.ts b/app/ydoc-shared/src/ast/tree.ts index 8aa8b4c7a6cd..7d98f44da4eb 100644 --- a/app/ydoc-shared/src/ast/tree.ts +++ b/app/ydoc-shared/src/ast/tree.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ import type { Identifier, IdentifierOrOperatorIdentifier, @@ -80,21 +81,33 @@ const astFieldKeys = allKeys({ metadata: null, }) +/** + * + */ export abstract class Ast { readonly module: Module /** @internal */ readonly fields: FixedMapView + /** + * + */ get id(): AstId { return this.fields.get('id') } + /** + * + */ get externalId(): ExternalId { const id = this.fields.get('metadata').get('externalId') assert(id != null) return id } + /** + * + */ get nodeMetadata(): NodeMetadata { const metadata = this.fields.get('metadata') return metadata as FixedMapView @@ -105,6 +118,9 @@ export abstract class Ast { return this.fields.get('metadata').toJSON() as any } + /** + * + */ typeName(): string { return this.fields.get('type') } @@ -116,27 +132,45 @@ export abstract class Ast { return this.id === other.id } + /** + * + */ innerExpression(): Ast { return this.wrappedExpression()?.innerExpression() ?? this } + /** + * + */ wrappedExpression(): Ast | undefined { return undefined } + /** + * + */ wrappingExpression(): Ast | undefined { const parent = this.parent() return parent?.wrappedExpression()?.is(this) ? parent : undefined } + /** + * + */ wrappingExpressionRoot(): Ast { return this.wrappingExpression()?.wrappingExpressionRoot() ?? this } + /** + * + */ documentingAncestor(): Documented | undefined { return this.wrappingExpression()?.documentingAncestor() } + /** + * + */ get isBindingStatement(): boolean { const inner = this.wrappedExpression() if (inner) { @@ -146,10 +180,16 @@ export abstract class Ast { } } + /** + * + */ code(): string { return print(this).code } + /** + * + */ visitRecursive(visit: (node: Ast | Token) => void): void { visit(this) for (const child of this.children()) { @@ -161,6 +201,9 @@ export abstract class Ast { } } + /** + * + */ visitRecursiveAst(visit: (ast: Ast) => void | boolean): void { if (visit(this) === false) return for (const child of this.children()) { @@ -168,6 +211,9 @@ export abstract class Ast { } } + /** + * + */ printSubtree( info: SpanMap, offset: number, @@ -189,19 +235,31 @@ export abstract class Ast { } } + /** + * + */ get parentId(): AstId | undefined { const parentId = this.fields.get('parent') if (parentId !== ROOT_ID) return parentId } + /** + * + */ parent(): Ast | undefined { return this.module.get(this.parentId) } + /** + * + */ static parseBlock(source: string, inModule?: MutableModule) { return parseBlock(source, inModule) } + /** + * + */ static parse(source: string, module?: MutableModule) { return parse(source, module) } @@ -213,25 +271,36 @@ export abstract class Ast { this.fields = fields } - /** @internal - * Returns child subtrees, including information about the whitespace between them. - */ + /** @internal */ abstract concreteChildren(verbatim?: boolean): IterableIterator } +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface MutableAst {} +/** + * + */ export abstract class MutableAst extends Ast { declare readonly module: MutableModule declare readonly fields: FixedMap + /** + * + */ setExternalId(id: ExternalId) { this.fields.get('metadata').set('externalId', id) } + /** + * + */ mutableNodeMetadata(): MutableNodeMetadata { const metadata = this.fields.get('metadata') return metadata as FixedMap } + /** + * + */ setNodeMetadata(nodeMeta: NodeMetadataFields) { const metadata = this.fields.get('metadata') as unknown as Map for (const [key, value] of Object.entries(nodeMeta)) { @@ -255,7 +324,8 @@ export abstract class MutableAst extends Ast { return asOwned(this) } - /** Change the value of the object referred to by the `target` ID. (The initial ID of `replacement` will be ignored.) + /** + * Change the value of the object referred to by the `target` ID. (The initial ID of `replacement` will be ignored.) * Returns the old value, with a new (unreferenced) ID. */ replaceValue(replacement: Owned): Owned { @@ -266,14 +336,19 @@ export abstract class MutableAst extends Ast { return old } + /** + * + */ replaceValueChecked(replacement: Owned): Owned { const parentId = this.fields.get('parent') assertDefined(parentId) return this.replaceValue(replacement) } - /** Replace the parent of this object with a reference to a new placeholder object. - * Returns the object, now parentless, and the placeholder. */ + /** + * Replace the parent of this object with a reference to a new placeholder object. + * Returns the object, now parentless, and the placeholder. + */ takeToReplace(): Removed { if (parentId(this)) { const placeholder = Wildcard.new(this.module) @@ -284,12 +359,17 @@ export abstract class MutableAst extends Ast { } } - /** Replace the parent of this object with a reference to a new placeholder object. - * Returns the object, now parentless. */ + /** + * Replace the parent of this object with a reference to a new placeholder object. + * Returns the object, now parentless. + */ take(): Owned { return this.replace(Wildcard.new(this.module)) } + /** + * + */ takeIfParented(): Owned { const parent = parentId(this) if (parent) { @@ -301,16 +381,18 @@ export abstract class MutableAst extends Ast { return asOwned(this) } - /** Replace the value assigned to the given ID with a placeholder. + /** + * Replace the value assigned to the given ID with a placeholder. * Returns the removed value, with a new unreferenced ID. - **/ + */ takeValue(): Removed { const placeholder = Wildcard.new(this.module) const node = this.replaceValue(placeholder) return { node, placeholder } } - /** Take this node from the tree, and replace it with the result of applying the given function to it. + /** + * Take this node from the tree, and replace it with the result of applying the given function to it. * * Note that this is a modification of the *parent* node. Any `Ast` objects or `AstId`s that pointed to the old value * will still point to the old value. @@ -323,7 +405,8 @@ export abstract class MutableAst extends Ast { return replacement } - /** Take this node from the tree, and replace it with the result of applying the given function to it; transfer the + /** + * Take this node from the tree, and replace it with the result of applying the given function to it; transfer the * metadata from this node to the replacement. * * Note that this is a modification of the *parent* node. Any `Ast` objects or `AstId`s that pointed to the old value @@ -337,6 +420,9 @@ export abstract class MutableAst extends Ast { return replacement } + /** + * + */ mutableParent(): MutableAst | undefined { const parentId = this.fields.get('parent') if (parentId === 'ROOT_ID') return @@ -353,6 +439,9 @@ export abstract class MutableAst extends Ast { applyTextEditsToAst(this, textEdits, metadataSource ?? this.module) } + /** + * + */ getOrInitDocumentation(): MutableDocumented { const existing = this.documentingAncestor() if (existing) return this.module.getVersion(existing) @@ -386,7 +475,13 @@ export abstract class MutableAst extends Ast { /** @internal */ claimChild(child: Owned): AstId + /** + * + */ claimChild(child: Owned | undefined): AstId | undefined + /** + * + */ claimChild(child: Owned | undefined): AstId | undefined { return child ? claimChild(this.module, child, this.id) : undefined } @@ -438,7 +533,8 @@ function idRewriter( } } -/** Apply the given function to each `AstId` in the fields of `ast`. For each value that it returns an output, that +/** + * Apply the given function to each `AstId` in the fields of `ast`. For each value that it returns an output, that * output will be substituted for the input ID. */ export function rewriteRefs(ast: MutableAst, f: (id: AstId) => AstId | undefined) { @@ -453,7 +549,8 @@ export function rewriteRefs(ast: MutableAst, f: (id: AstId) => AstId | undefined return fieldsChanged } -/** Copy all fields except the `Ast` base fields from `ast2` to `ast1`. A reference-rewriting function will be applied +/** + * Copy all fields except the `Ast` base fields from `ast2` to `ast1`. A reference-rewriting function will be applied * to `AstId`s in copied fields; see {@link rewriteRefs}. */ export function syncFields(ast1: MutableAst, ast2: Ast, f: (id: AstId) => AstId | undefined) { @@ -463,6 +560,9 @@ export function syncFields(ast1: MutableAst, ast2: Ast, f: (id: AstId) => AstId } } +/** + * + */ export function syncNodeMetadata(target: MutableNodeMetadata, source: NodeMetadata) { const oldPos = target.get('position') const newPos = source.get('position') @@ -590,17 +690,29 @@ interface NameSpecification { name: T['token'] equals: T['token'] } +/** + * + */ export class App extends Ast { declare fields: FixedMap + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableApp) return parsed } + /** + * + */ static concrete( module: MutableModule, func: NodeChild, @@ -619,6 +731,9 @@ export class App extends Ast { return asOwned(new MutableApp(module, fields)) } + /** + * + */ static new( module: MutableModule, func: Owned, @@ -634,10 +749,16 @@ export class App extends Ast { ) } + /** + * + */ static positional(func: Owned, argument: Owned, module?: MutableModule): Owned { return App.new(module ?? MutableModule.Transient(), func, undefined, argument) } + /** + * + */ static PositionalSequence(func: Owned, args: Owned[]): Owned { return args.reduce( (expression, argument) => App.new(func.module, expression, undefined, argument), @@ -645,16 +766,28 @@ export class App extends Ast { ) } + /** + * + */ get function(): Ast { return this.module.get(this.fields.get('function').node) } + /** + * + */ get argumentName(): Token | undefined { return this.module.getToken(this.fields.get('nameSpecification')?.name.node) } + /** + * + */ get argument(): Ast { return this.module.get(this.fields.get('argument').node) } + /** + * + */ *concreteChildren(verbatim?: boolean): IterableIterator { const { function: function_, parens, nameSpecification, argument } = getAll(this.fields) yield ensureUnspaced(function_, verbatim) @@ -708,6 +841,9 @@ function preferUnspaced(child: NodeChild): ConcreteChild { function preferSpaced(child: NodeChild): ConcreteChild { return tryAsConcrete(child) ?? { ...child, whitespace: ' ' } } +/** + * + */ export class MutableApp extends App implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -732,17 +868,29 @@ interface UnaryOprAppFields { operator: NodeChild argument: NodeChild | undefined } +/** + * + */ export class UnaryOprApp extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableUnaryOprApp) return parsed } + /** + * + */ static concrete( module: MutableModule, operator: NodeChild, @@ -757,23 +905,38 @@ export class UnaryOprApp extends Ast { return asOwned(new MutableUnaryOprApp(module, fields)) } + /** + * + */ static new(module: MutableModule, operator: Token, argument: Owned | undefined) { return this.concrete(module, unspaced(operator), argument ? autospaced(argument) : undefined) } + /** + * + */ get operator(): Token { return this.module.getToken(this.fields.get('operator').node) } + /** + * + */ get argument(): Ast | undefined { return this.module.get(this.fields.get('argument')?.node) } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const { operator, argument } = getAll(this.fields) yield operator if (argument) yield argument } } +/** + * + */ export class MutableUnaryOprApp extends UnaryOprApp implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -794,16 +957,28 @@ interface AutoscopedIdentifierFields { operator: NodeChild identifier: NodeChild } +/** + * + */ export class AutoscopedIdentifier extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ get identifier(): Token { return this.module.getToken(this.fields.get('identifier').node) } + /** + * + */ static tryParse( source: string, module?: MutableModule, @@ -812,6 +987,9 @@ export class AutoscopedIdentifier extends Ast { if (parsed instanceof MutableAutoscopedIdentifier) return parsed } + /** + * + */ static concrete(module: MutableModule, operator: NodeChild, identifier: NodeChild) { const base = module.baseObject('AutoscopedIdentifier') const fields = composeFieldData(base, { @@ -821,6 +999,9 @@ export class AutoscopedIdentifier extends Ast { return asOwned(new MutableAutoscopedIdentifier(module, fields)) } + /** + * + */ static new( identifier: TypeOrConstructorIdentifier, module?: MutableModule, @@ -831,12 +1012,18 @@ export class AutoscopedIdentifier extends Ast { return this.concrete(module_, unspaced(operator), unspaced(ident)) } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const { operator, identifier } = getAll(this.fields) yield operator yield identifier } } +/** + * + */ export class MutableAutoscopedIdentifier extends AutoscopedIdentifier implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -853,17 +1040,29 @@ interface NegationAppFields { operator: NodeChild argument: NodeChild } +/** + * + */ export class NegationApp extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableNegationApp) return parsed } + /** + * + */ static concrete(module: MutableModule, operator: NodeChild, argument: NodeChild) { const base = module.baseObject('NegationApp') const id_ = base.get('id') @@ -874,24 +1073,39 @@ export class NegationApp extends Ast { return asOwned(new MutableNegationApp(module, fields)) } + /** + * + */ static new(module: MutableModule, argument: Owned) { const minus = Token.new('-', RawAst.Token.Type.Operator) return this.concrete(module, unspaced(minus), unspaced(argument)) } + /** + * + */ get operator(): Token { return this.module.getToken(this.fields.get('operator').node) } + /** + * + */ get argument(): Ast { return this.module.get(this.fields.get('argument').node) } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const { operator, argument } = getAll(this.fields) yield operator if (argument) yield argument } } +/** + * + */ export class MutableNegationApp extends NegationApp implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -910,17 +1124,29 @@ interface OprAppFields { operators: NodeChild[] rhs: NodeChild | undefined } +/** + * + */ export class OprApp extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableOprApp) return parsed } + /** + * + */ static concrete( module: MutableModule, lhs: NodeChild | undefined, @@ -937,6 +1163,9 @@ export class OprApp extends Ast { return asOwned(new MutableOprApp(module, fields)) } + /** + * + */ static new( module: MutableModule, lhs: Owned | undefined, @@ -948,9 +1177,15 @@ export class OprApp extends Ast { return OprApp.concrete(module, unspaced(lhs), [autospaced(operatorToken)], autospaced(rhs)) } + /** + * + */ get lhs(): Ast | undefined { return this.module.get(this.fields.get('lhs')?.node) } + /** + * + */ get operator(): Result[]> { const operators = this.fields.get('operators') const operators_ = operators.map(child => ({ @@ -960,10 +1195,16 @@ export class OprApp extends Ast { const [opr] = operators_ return opr ? Ok(opr.node) : Err(operators_) } + /** + * + */ get rhs(): Ast | undefined { return this.module.get(this.fields.get('rhs')?.node) } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const { lhs, operators, rhs } = getAll(this.fields) if (lhs) yield lhs @@ -971,6 +1212,9 @@ export class OprApp extends Ast { if (rhs) yield rhs } } +/** + * + */ export class MutableOprApp extends OprApp implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -996,12 +1240,21 @@ interface PropertyAccessFields { operator: NodeChild rhs: NodeChild } +/** + * + */ export class PropertyAccess extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse( source: string, module?: MutableModule, @@ -1010,6 +1263,9 @@ export class PropertyAccess extends Ast { if (parsed instanceof MutablePropertyAccess) return parsed } + /** + * + */ static new(module: MutableModule, lhs: Owned, rhs: IdentLike, style?: { spaced?: boolean }) { const dot = Token.new('.', RawAst.Token.Type.Operator) const whitespace = style?.spaced ? ' ' : '' @@ -1021,18 +1277,30 @@ export class PropertyAccess extends Ast { ) } + /** + * + */ static Sequence( segments: [StrictIdentLike, ...StrictIdentLike[]], module: MutableModule, ): Owned | Owned + /** + * + */ static Sequence( segments: [StrictIdentLike, ...StrictIdentLike[], IdentLike], module: MutableModule, ): Owned | Owned + /** + * + */ static Sequence( segments: IdentLike[], module: MutableModule, ): Owned | Owned | undefined + /** + * + */ static Sequence( segments: IdentLike[], module: MutableModule, @@ -1047,6 +1315,9 @@ export class PropertyAccess extends Ast { if (!operatorInNonFinalSegment) return path } + /** + * + */ static concrete( module: MutableModule, lhs: NodeChild | undefined, @@ -1063,18 +1334,30 @@ export class PropertyAccess extends Ast { return asOwned(new MutablePropertyAccess(module, fields)) } + /** + * + */ get lhs(): Ast | undefined { return this.module.get(this.fields.get('lhs')?.node) } + /** + * + */ get operator(): Token { return this.module.getToken(this.fields.get('operator').node) } + /** + * + */ get rhs(): IdentifierOrOperatorIdentifierToken { const ast = this.module.get(this.fields.get('rhs').node) assert(ast instanceof Ident) return ast.token as IdentifierOrOperatorIdentifierToken } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const { lhs, operator, rhs } = getAll(this.fields) if (lhs) yield lhs @@ -1082,6 +1365,9 @@ export class PropertyAccess extends Ast { yield rhs } } +/** + * + */ export class MutablePropertyAccess extends PropertyAccess implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -1100,8 +1386,10 @@ export interface MutablePropertyAccess extends PropertyAccess, MutableAst { } applyMixins(MutablePropertyAccess, [MutableAst]) -/** Unroll the provided chain of `PropertyAccess` nodes, returning the first non-access as `subject` and the accesses - * from left-to-right. */ +/** + * Unroll the provided chain of `PropertyAccess` nodes, returning the first non-access as `subject` and the accesses + * from left-to-right. + */ export function accessChain(ast: Ast): { subject: Ast; accessChain: PropertyAccess[] } { const accessChain = new Array() while (ast instanceof PropertyAccess && ast.lhs) { @@ -1115,12 +1403,21 @@ export function accessChain(ast: Ast): { subject: Ast; accessChain: PropertyAcce interface GenericFields { children: RawNodeChild[] } +/** + * + */ export class Generic extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static concrete(module: MutableModule, children: (NodeChild | NodeChild)[]) { const base = module.baseObject('Generic') const id_ = base.get('id') @@ -1130,10 +1427,16 @@ export class Generic extends Ast { return asOwned(new MutableGeneric(module, fields)) } + /** + * + */ concreteChildren(_verbatim?: boolean): IterableIterator { return this.fields.get('children')[Symbol.iterator]() } } +/** + * + */ export class MutableGeneric extends Generic implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -1183,36 +1486,66 @@ interface ImportFields extends FieldObject { hiding: MultiSegmentAppSegment | undefined } +/** + * + */ export class Import extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableImport) return parsed } + /** + * + */ get polyglot(): Ast | undefined { return this.module.get(this.fields.get('polyglot')?.body?.node) } + /** + * + */ get from(): Ast | undefined { return this.module.get(this.fields.get('from')?.body?.node) } + /** + * + */ get import_(): Ast | undefined { return this.module.get(this.fields.get('import').body?.node) } + /** + * + */ get all(): Token | undefined { return this.module.getToken(this.fields.get('all')?.node) } + /** + * + */ get as(): Ast | undefined { return this.module.get(this.fields.get('as')?.body?.node) } + /** + * + */ get hiding(): Ast | undefined { return this.module.get(this.fields.get('hiding')?.body?.node) } + /** + * + */ static concrete( module: MutableModule, polyglot: MultiSegmentAppSegment | undefined, @@ -1237,6 +1570,9 @@ export class Import extends Ast { return asOwned(new MutableImport(module, fields)) } + /** + * + */ static Qualified(path: IdentLike[], module: MutableModule): Owned | undefined { const path_ = PropertyAccess.Sequence(path, module) if (!path_) return @@ -1251,6 +1587,9 @@ export class Import extends Ast { ) } + /** + * + */ static Unqualified( path: IdentLike[], name: IdentLike, @@ -1270,6 +1609,9 @@ export class Import extends Ast { ) } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const segment = (segment: MultiSegmentAppSegment | undefined) => { const parts = [] @@ -1286,6 +1628,9 @@ export class Import extends Ast { yield* segment(hiding) } } +/** + * + */ export class MutableImport extends Import implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -1435,17 +1780,29 @@ interface TextLiteralFields { elements: TextElement[] close: NodeChild | undefined } +/** + * + */ export class TextLiteral extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableTextLiteral) return parsed } + /** + * + */ static concrete( module: MutableModule, open: NodeChild | undefined, @@ -1464,6 +1821,9 @@ export class TextLiteral extends Ast { return asOwned(new MutableTextLiteral(module, fields)) } + /** + * + */ static new(rawText: string, module?: MutableModule): Owned { const escaped = escapeTextLiteral(rawText) const parsed = parse(`'${escaped}'`, module) @@ -1483,6 +1843,9 @@ export class TextLiteral extends Ast { return uninterpolatedText(this.fields.get('elements'), this.module) } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const { open, newline, elements, close } = getAll(this.fields) if (open) yield open @@ -1491,27 +1854,45 @@ export class TextLiteral extends Ast { if (close) yield close } + /** + * + */ boundaryTokenCode(): string | undefined { return (this.open || this.close)?.code() } + /** + * + */ isInterpolated(): boolean { const token = this.boundaryTokenCode() return token === "'" || token === "'''" } + /** + * + */ get open(): Token | undefined { return this.module.getToken(this.fields.get('open')?.node) } + /** + * + */ get close(): Token | undefined { return this.module.getToken(this.fields.get('close')?.node) } + /** + * + */ get elements(): TextElement[] { return this.fields.get('elements').map(e => mapRefs(e, rawToConcrete(this.module))) } } +/** + * + */ export class MutableTextLiteral extends TextLiteral implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -1557,17 +1938,29 @@ interface DocumentedFields { newlines: NodeChild[] expression: NodeChild | undefined } +/** + * + */ export class Documented extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableDocumented) return parsed } + /** + * + */ static new(text: string, expression: Owned) { return this.concrete( expression.module, @@ -1578,6 +1971,9 @@ export class Documented extends Ast { ) } + /** + * + */ static concrete( module: MutableModule, open: NodeChild | undefined, @@ -1596,6 +1992,9 @@ export class Documented extends Ast { return asOwned(new MutableDocumented(module, fields)) } + /** + * + */ get expression(): Ast | undefined { return this.module.get(this.fields.get('expression')?.node) } @@ -1606,14 +2005,23 @@ export class Documented extends Ast { return raw.startsWith(' ') ? raw.slice(1) : raw } + /** + * + */ override wrappedExpression(): Ast | undefined { return this.expression } + /** + * + */ override documentingAncestor(): Documented | undefined { return this } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const { open, elements, newlines, expression } = getAll(this.fields) yield open @@ -1622,6 +2030,9 @@ export class Documented extends Ast { if (expression) yield expression } + /** + * + */ override printSubtree( info: SpanMap, offset: number, @@ -1631,6 +2042,9 @@ export class Documented extends Ast { return printDocumented(this, info, offset, parentIndent, verbatim) } } +/** + * + */ export class MutableDocumented extends Documented implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -1672,25 +2086,43 @@ function textToUninterpolatedElements(text: string): TextToken[] { interface InvalidFields { expression: NodeChild } +/** + * + */ export class Invalid extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static concrete(module: MutableModule, expression: NodeChild) { const base = module.baseObject('Invalid') return asOwned(new MutableInvalid(module, invalidFields(module, base, expression))) } + /** + * + */ get expression(): Ast { return this.module.get(this.fields.get('expression').node) } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { yield this.fields.get('expression') } + /** + * + */ override printSubtree( info: SpanMap, offset: number, @@ -1700,6 +2132,9 @@ export class Invalid extends Ast { return super.printSubtree(info, offset, parentIndent, true) } } +/** + * + */ export function invalidFields( module: MutableModule, base: FixedMap, @@ -1708,13 +2143,18 @@ export function invalidFields( const id_ = base.get('id') return composeFieldData(base, { expression: concreteChild(module, expression, id_) }) } +/** + * + */ export class MutableInvalid extends Invalid implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap } export interface MutableInvalid extends Invalid, MutableAst { - /** The `expression` getter is intentionally not narrowed to provide mutable access: - * It makes more sense to `.replace` the `Invalid` node. */ + /** + * The `expression` getter is intentionally not narrowed to provide mutable access: + * It makes more sense to `.replace` the `Invalid` node. + */ } applyMixins(MutableInvalid, [MutableAst]) @@ -1723,17 +2163,29 @@ interface GroupFields { expression: NodeChild | undefined close: NodeChild | undefined } +/** + * + */ export class Group extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableGroup) return parsed } + /** + * + */ static concrete( module: MutableModule, open: NodeChild | undefined, @@ -1750,20 +2202,32 @@ export class Group extends Ast { return asOwned(new MutableGroup(module, fields)) } + /** + * + */ static new(module: MutableModule, expression: Owned) { const open = unspaced(Token.new('(', RawAst.Token.Type.OpenSymbol)) const close = unspaced(Token.new(')', RawAst.Token.Type.CloseSymbol)) return this.concrete(module, open, unspaced(expression), close) } + /** + * + */ get expression(): Ast | undefined { return this.module.get(this.fields.get('expression')?.node) } + /** + * + */ override wrappedExpression(): Ast | undefined { return this.expression } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const { open, expression, close } = getAll(this.fields) if (open) yield open @@ -1771,6 +2235,9 @@ export class Group extends Ast { if (close) yield close } } +/** + * + */ export class MutableGroup extends Group implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -1787,12 +2254,21 @@ applyMixins(MutableGroup, [MutableAst]) interface NumericLiteralFields { tokens: NodeChild[] } +/** + * + */ export class NumericLiteral extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse( source: string, module?: MutableModule, @@ -1801,6 +2277,9 @@ export class NumericLiteral extends Ast { if (parsed instanceof MutableNumericLiteral) return parsed } + /** + * + */ static tryParseWithSign( source: string, module?: MutableModule, @@ -1813,16 +2292,25 @@ export class NumericLiteral extends Ast { return parsed } + /** + * + */ static concrete(module: MutableModule, tokens: NodeChild[]) { const base = module.baseObject('NumericLiteral') const fields = composeFieldData(base, { tokens }) return asOwned(new MutableNumericLiteral(module, fields)) } + /** + * + */ concreteChildren(_verbatim?: boolean): IterableIterator { return this.fields.get('tokens')[Symbol.iterator]() } } +/** + * + */ export class MutableNumericLiteral extends NumericLiteral implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -1830,6 +2318,9 @@ export class MutableNumericLiteral extends NumericLiteral implements MutableAst export interface MutableNumericLiteral extends NumericLiteral, MutableAst {} applyMixins(MutableNumericLiteral, [MutableAst]) +/** + * + */ export function isNumericLiteral(code: string) { return is_numeric_literal(code) } @@ -1861,29 +2352,50 @@ export interface FunctionFields { equals: NodeChild body: NodeChild | undefined } +/** + * + */ export class Function extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableFunction) return parsed } + /** + * + */ get name(): Ast { return this.module.get(this.fields.get('name').node) } + /** + * + */ get body(): Ast | undefined { return this.module.get(this.fields.get('body')?.node) } + /** + * + */ get argumentDefinitions(): ArgumentDefinition[] { return this.fields .get('argumentDefinitions') .map(def => mapRefs(def, rawToConcrete(this.module))) } + /** + * + */ static concrete( module: MutableModule, name: NodeChild, @@ -1902,6 +2414,9 @@ export class Function extends Ast { return asOwned(new MutableFunction(module, fields)) } + /** + * + */ static new( module: MutableModule, name: IdentLike, @@ -1937,6 +2452,9 @@ export class Function extends Ast { return MutableFunction.new(module, name, argumentDefinitions, body) } + /** + * + */ *bodyExpressions(): IterableIterator { const body = this.body if (body instanceof BodyBlock) { @@ -1946,10 +2464,16 @@ export class Function extends Ast { } } + /** + * + */ override get isBindingStatement(): boolean { return true } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { const { name, argumentDefinitions, equals, body } = getAll(this.fields) yield name @@ -1974,6 +2498,9 @@ export class Function extends Ast { if (body) yield preferSpacedIf(body, this.module.tryGet(body.node) instanceof BodyBlock) } } +/** + * + */ export class MutableFunction extends Function implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -2012,17 +2539,29 @@ interface AssignmentFields { equals: NodeChild expression: NodeChild } +/** + * + */ export class Assignment extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableAssignment) return parsed } + /** + * + */ static concrete( module: MutableModule, pattern: NodeChild, @@ -2039,6 +2578,9 @@ export class Assignment extends Ast { return asOwned(new MutableAssignment(module, fields)) } + /** + * + */ static new(module: MutableModule, ident: StrictIdentLike, expression: Owned) { return Assignment.concrete( module, @@ -2048,17 +2590,29 @@ export class Assignment extends Ast { ) } + /** + * + */ get pattern(): Ast { return this.module.get(this.fields.get('pattern').node) } + /** + * + */ get expression(): Ast { return this.module.get(this.fields.get('expression').node) } + /** + * + */ override get isBindingStatement(): boolean { return true } + /** + * + */ *concreteChildren(verbatim?: boolean): IterableIterator { const { pattern, equals, expression } = getAll(this.fields) yield ensureUnspaced(pattern, verbatim) @@ -2066,6 +2620,9 @@ export class Assignment extends Ast { yield preferSpaced(expression) } } +/** + * + */ export class MutableAssignment extends Assignment implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -2086,17 +2643,29 @@ applyMixins(MutableAssignment, [MutableAst]) interface BodyBlockFields { lines: RawBlockLine[] } +/** + * + */ export class BodyBlock extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableBodyBlock) return parsed } + /** + * + */ static concrete(module: MutableModule, lines: OwnedBlockLine[]) { const base = module.baseObject('BodyBlock') const id_ = base.get('id') @@ -2106,20 +2675,32 @@ export class BodyBlock extends Ast { return asOwned(new MutableBodyBlock(module, fields)) } + /** + * + */ static new(lines: OwnedBlockLine[], module: MutableModule) { return BodyBlock.concrete(module, lines) } + /** + * + */ get lines(): BlockLine[] { return this.fields.get('lines').map(line => lineFromRaw(line, this.module)) } + /** + * + */ *statements(): IterableIterator { for (const line of this.lines) { if (line.expression) yield line.expression.node } } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { for (const line of this.fields.get('lines')) { yield preferUnspaced(line.newline) @@ -2127,6 +2708,9 @@ export class BodyBlock extends Ast { } } + /** + * + */ override printSubtree( info: SpanMap, offset: number, @@ -2136,6 +2720,9 @@ export class BodyBlock extends Ast { return printBlock(this, info, offset, parentIndent, verbatim) } } +/** + * + */ export class MutableBodyBlock extends BodyBlock implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -2245,31 +2832,52 @@ function lineToRaw(line: OwnedBlockLine, module: MutableModule, block: AstId): R interface IdentFields { token: NodeChild } +/** + * + */ export class Ident extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableIdent) return parsed } + /** + * + */ get token(): IdentifierToken { return this.module.getToken(this.fields.get('token').node) as IdentifierToken } + /** + * + */ isTypeOrConstructor(): boolean { return /^[A-Z]/.test(this.token.code()) } + /** + * + */ static concrete(module: MutableModule, token: NodeChild) { const base = module.baseObject('Ident') const fields = composeFieldData(base, { token }) return asOwned(new MutableIdent(module, fields)) } + /** + * + */ static new(module: MutableModule, ident: StrictIdentLike) { return Ident.concrete(module, unspaced(toIdentStrict(ident))) } @@ -2279,14 +2887,23 @@ export class Ident extends Ast { return Ident.concrete(module, unspaced(toIdent(ident))) } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { yield this.fields.get('token') } + /** + * + */ override code(): Identifier { return this.token.code() as Identifier } } +/** + * + */ export class MutableIdent extends Ident implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -2305,37 +2922,61 @@ applyMixins(MutableIdent, [MutableAst]) interface WildcardFields { token: NodeChild } +/** + * + */ export class Wildcard extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableWildcard) return parsed } + /** + * + */ get token(): Token { return this.module.getToken(this.fields.get('token').node) } + /** + * + */ static concrete(module: MutableModule, token: NodeChild) { const base = module.baseObject('Wildcard') const fields = composeFieldData(base, { token }) return asOwned(new MutableWildcard(module, fields)) } + /** + * + */ static new(module?: MutableModule) { const token = Token.new('_', RawAst.Token.Type.Wildcard) return this.concrete(module ?? MutableModule.Transient(), unspaced(token)) } + /** + * + */ *concreteChildren(_verbatim?: boolean): IterableIterator { yield this.fields.get('token') } } +/** + * + */ export class MutableWildcard extends Wildcard implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -2359,17 +3000,29 @@ interface VectorFields { elements: VectorElement[] close: NodeChild } +/** + * + */ export class Vector extends Ast { declare fields: FixedMapView + /** + * + */ constructor(module: Module, fields: FixedMapView) { super(module, fields) } + /** + * + */ static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableVector) return parsed } + /** + * + */ static concrete( module: MutableModule, open: NodeChild | undefined, @@ -2386,6 +3039,9 @@ export class Vector extends Ast { return asOwned(new MutableVector(module, fields)) } + /** + * + */ static new(module: MutableModule, elements: Owned[]) { return this.concrete( module, @@ -2395,16 +3051,25 @@ export class Vector extends Ast { ) } + /** + * + */ static tryBuild( inputs: Iterable, elementBuilder: (input: T, module: MutableModule) => Owned, edit?: MutableModule, ): Owned + /** + * + */ static tryBuild( inputs: Iterable, elementBuilder: (input: T, module: MutableModule) => Owned | undefined, edit?: MutableModule, ): Owned | undefined + /** + * + */ static tryBuild( inputs: Iterable, valueBuilder: (input: T, module: MutableModule) => Owned | undefined, @@ -2420,6 +3085,9 @@ export class Vector extends Ast { return Vector.concrete(module, undefined, elements, undefined) } + /** + * + */ static build( inputs: Iterable, elementBuilder: (input: T, module: MutableModule) => Owned, @@ -2428,6 +3096,9 @@ export class Vector extends Ast { return Vector.tryBuild(inputs, elementBuilder, edit) } + /** + * + */ *concreteChildren(verbatim?: boolean): IterableIterator { const { open, elements, close } = getAll(this.fields) yield ensureUnspaced(open, verbatim) @@ -2444,21 +3115,33 @@ export class Vector extends Ast { yield preferUnspaced(close) } + /** + * + */ *values(): IterableIterator { for (const element of this.fields.get('elements')) if (element.value) yield this.module.get(element.value.node) } + /** + * + */ *enumerate(): IterableIterator<[number, Ast | undefined]> { for (const [index, element] of this.fields.get('elements').entries()) { yield [index, this.module.get(element.value?.node)] } } + /** + * + */ get length() { return this.fields.get('elements').length } } +/** + * + */ export class MutableVector extends Vector implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap @@ -2534,6 +3217,9 @@ export type Mutable = : T extends Wildcard ? MutableWildcard : MutableAst +/** + * + */ export function materializeMutable(module: MutableModule, fields: FixedMap): MutableAst { const type = fields.get('type') const fieldsForType = fields as FixedMap @@ -2580,6 +3266,9 @@ export function materializeMutable(module: MutableModule, fields: FixedMap): Ast { const type = fields.get('type') const fields_ = fields as FixedMapView @@ -2628,7 +3317,7 @@ export function materialize(module: Module, fields: FixedMapView): As export interface FixedMapView { get(key: Key): DeepReadonly - /** @internal Unsafe. The caller must ensure the yielded values are not modified. */ + /** @internal */ entries(): IterableIterator clone(): FixedMap has(key: string): boolean @@ -2648,7 +3337,8 @@ function getAll(map: FixedMapView): DeepReadonlyF } declare const brandLegalFieldContent: unique symbol -/** Used to add a constraint to all `AstFields`s subtypes ensuring that they were produced by `composeFieldData`, which +/** + * Used to add a constraint to all `AstFields`s subtypes ensuring that they were produced by `composeFieldData`, which * enforces a requirement that the provided fields extend `FieldObject`. */ interface LegalFieldContent { @@ -2668,8 +3358,10 @@ export function setAll>( return map_ } -/** Modifies the input `map`. Returns the same object with an extended type. The added fields are required to have only - * types extending `FieldData`; the returned object is branded as `LegalFieldContent`. */ +/** + * Modifies the input `map`. Returns the same object with an extended type. The added fields are required to have only + * types extending `FieldData`; the returned object is branded as `LegalFieldContent`. + */ export function composeFieldData>( map: FixedMap, fields: Fields2, @@ -2791,6 +3483,9 @@ function unspaced(node: T | undefined): NodeChild export function autospaced(node: T): NodeChild export function autospaced(node: T | undefined): NodeChild | undefined +/** + * + */ export function autospaced( node: T | undefined, ): NodeChild | undefined { diff --git a/app/ydoc-shared/src/binaryProtocol.ts b/app/ydoc-shared/src/binaryProtocol.ts index c7ca3649d344..b70e371536e4 100644 --- a/app/ydoc-shared/src/binaryProtocol.ts +++ b/app/ydoc-shared/src/binaryProtocol.ts @@ -236,6 +236,9 @@ export type Table = { bbPos: number } +/** + * A helper to incrementally build a message buffer. + */ export class Builder { private bb: ByteBuffer private space: number @@ -249,12 +252,18 @@ export class Builder { private _forceDefaults = false private stringMaps: Map | null = null + /** + * Create a {@link Builder}. + */ constructor(initialSize?: number) { initialSize ??= 1024 this.bb = new ByteBuffer(new ArrayBuffer(initialSize)) this.space = initialSize } + /** + * Reset the state of the internal buffer. + */ clear(): void { this.bb.position = 0 this.space = this.bb.view.byteLength @@ -273,17 +282,22 @@ export class Builder { * In order to save space, fields that are set to their default value * don't get serialized into the buffer. Forcing defaults provides a * way to manually disable this optimization. - * * @param forceDefaults true always serializes default values */ forceDefaults(forceDefaults: boolean): void { this._forceDefaults = forceDefaults } + /** + * Return the current contents as an {@link ArrayBuffer}. + */ toArrayBuffer(): ArrayBuffer { return this.bb.view.buffer.slice(this.bb.position, this.bb.position + this.offset()) } + /** + * Ensure alignment of the next field, and grow the byte buffer if needed. + */ prep(size: number, additionalBytes: number): void { if (size > this.minAlignment) { this.minAlignment = size @@ -300,66 +314,114 @@ export class Builder { this.pad(alignSize) } + /** + * Add padding to the backing {@link ByteBuffer} for alignment purposes. + */ pad(byteSize: number): void { for (let i = 0; i < byteSize; i++) { this.bb.view.setInt8(--this.space, 0) } } + /** + * Write a signed 8-bit integer and update the buffer position. + * Prefer {@link addInt8} which ensures alignment as well. + */ writeInt8(value: number): void { this.bb.view.setInt8((this.space -= 1), value) } + /** + * Write a signed 16-bit integer and update the buffer position. + * Prefer {@link addInt16} which ensures alignment as well. + */ writeInt16(value: number): void { this.bb.view.setInt16((this.space -= 2), value, true) } + /** + * Write a signed 32-bit integer and update the buffer position. + * Prefer {@link addInt32} which ensures alignment as well. + */ writeInt32(value: number): void { this.bb.view.setInt32((this.space -= 4), value, true) } + /** + * Write a signed 64-bit integer and update the buffer position. + * Prefer {@link addInt64} which ensures alignment as well. + */ writeInt64(value: bigint): void { this.bb.view.setBigInt64((this.space -= 8), value, true) } + /** + * Write a 32-bit IEEE754 floating point number and update the buffer position. + * Prefer {@link addFloat32} which ensures alignment as well. + */ writeFloat32(value: number): void { this.bb.view.setFloat32((this.space -= 4), value, true) } + /** + * Write a 64-bit IEEE754 floating point number and update the buffer position. + * Prefer {@link addFloat64} which ensures alignment as well. + */ writeFloat64(value: number): void { this.bb.view.setFloat64((this.space -= 8), value, true) } + /** + * Ensure alignment and write a signed 8-bit integer and update the buffer position. + */ addInt8(value: number): void { this.prep(1, 0) this.writeInt8(value) } + /** + * Ensure alignment and write a signed 16-bit integer and update the buffer position. + */ addInt16(value: number): void { this.prep(2, 0) this.writeInt16(value) } + /** + * Ensure alignment and write a signed 32-bit integer and update the buffer position. + */ addInt32(value: number): void { this.prep(4, 0) this.writeInt32(value) } + /** + * Ensure alignment and write a signed 64-bit integer and update the buffer position. + */ addInt64(value: bigint): void { this.prep(8, 0) this.writeInt64(value) } + /** + * Ensure alignment and write a 32-bit IEEE754 floating point number and update the buffer position. + */ addFloat32(value: number): void { this.prep(4, 0) this.writeFloat32(value) } + /** + * Ensure alignment and write a 64-bit IEEE754 floating point number and update the buffer position. + */ addFloat64(value: number): void { this.prep(8, 0) this.writeFloat64(value) } + /** + * + */ addFieldInt8(voffset: number, value: number, defaultValue: number | null): void { if (this._forceDefaults || value != defaultValue) { this.addInt8(value) @@ -367,6 +429,9 @@ export class Builder { } } + /** + * + */ addFieldInt16(voffset: number, value: number, defaultValue: number | null): void { if (this._forceDefaults || value != defaultValue) { this.addInt16(value) @@ -374,6 +439,9 @@ export class Builder { } } + /** + * + */ addFieldInt32(voffset: number, value: number, defaultValue: number | null): void { if (this._forceDefaults || value != defaultValue) { this.addInt32(value) @@ -381,6 +449,9 @@ export class Builder { } } + /** + * + */ addFieldInt64(voffset: number, value: bigint, defaultValue: bigint | null): void { if (this._forceDefaults || value !== defaultValue) { this.addInt64(value) @@ -388,6 +459,9 @@ export class Builder { } } + /** + * + */ addFieldFloat32(voffset: number, value: number, defaultValue: number | null): void { if (this._forceDefaults || value != defaultValue) { this.addFloat32(value) @@ -395,6 +469,9 @@ export class Builder { } } + /** + * + */ addFieldFloat64(voffset: number, value: number, defaultValue: number | null): void { if (this._forceDefaults || value != defaultValue) { this.addFloat64(value) @@ -402,6 +479,9 @@ export class Builder { } } + /** + * + */ addFieldOffset( voffset: number, value: Offset, @@ -413,6 +493,9 @@ export class Builder { } } + /** + * + */ addFieldStruct( voffset: number, value: Offset, @@ -424,26 +507,42 @@ export class Builder { } } + /** + * + */ nested(obj: AnyOffset): void { if (obj != this.offset()) { throw new TypeError('FlatBuffers: struct must be serialized inline.') } } + /** + * Assert that the builder is not already serializing an object when beginning object serialization. + */ notNested(): void { if (this.isNested) { throw new TypeError('FlatBuffers: object serialization must not be nested.') } } + /** + * + */ slot(voffset: number): void { if (this.vtable !== null) this.vtable[voffset] = this.offset() } + /** + * Return the offset of the current filled content of the backing {@link ByteBuffer}. + */ offset(): Offset { return (this.bb.view.byteLength - this.space) as Offset } + /** + * Grow the internal {@link ByteBuffer}. + * Only call this if there is not enough space for the next write. + */ static growByteBuffer(bb: ByteBuffer): void { const oldBufSize = bb.view.byteLength // Ensure we don't grow beyond what fits in an int. @@ -457,11 +556,17 @@ export class Builder { bb.view = new DataView(newBuffer) } + /** + * Write an offset to a given field. + */ addOffset(offset: AnyOffset): void { this.prep(SIZEOF_INT, 0) // Ensure alignment is already done. this.writeInt32(this.offset() - offset + SIZEOF_INT) } + /** + * + */ startObject(numfields: number): void { this.notNested() if (this.vtable == null) { @@ -475,6 +580,9 @@ export class Builder { this.objectStart = this.offset() } + /** + * + */ endObject(): Offset { if (this.vtable == null || !this.isNested) { throw new globalThis.Error('FlatBuffers: endObject called without startObject') @@ -536,6 +644,9 @@ export class Builder { return vtableloc as Offset } + /** + * Finalize a message to be ready for sending. + */ finish( rootTable: AnyOffset, fileIdentifier?: string, @@ -560,11 +671,17 @@ export class Builder { return this } + /** + * + */ finishSizePrefixed(rootTable: AnyOffset, fileIdentifier?: string): Builder { this.finish(rootTable, fileIdentifier, true) return this } + /** + * + */ requiredField(table: AnyOffset, field: number): void { const tableStart = this.bb.view.byteLength - table const vtableStart = tableStart - this.bb.view.getInt32(tableStart, true) @@ -577,6 +694,9 @@ export class Builder { } } + /** + * Initialize buffer state for adding vector elements. + */ startVector(elemSize: number, numElems: number, alignment: number): void { this.notNested() this.vectorNumElems = numElems @@ -584,11 +704,17 @@ export class Builder { this.prep(alignment, elemSize * numElems) // Just in case alignment > int. } + /** + * Finish buffer state after having added all vector elements. + */ endVector(): Offset { this.writeInt32(this.vectorNumElems) return this.offset() } + /** + * Add a shared string to this buffer. + */ createSharedString(s: T): Offset { if (!s) { return 0 as Offset @@ -605,6 +731,9 @@ export class Builder { return offset as Offset } + /** + * Add a string to this buffer. + */ createString(s: string | Uint8Array | ArrayBuffer | null | undefined): Offset { if (s === null || s === undefined) return Null let utf8: string | Uint8Array | number[] @@ -624,6 +753,9 @@ export class Builder { return this.endVector() } + /** + * + */ createObjectOffset( obj: T extends string | null ? T : IGeneratedObject, ): Offset { @@ -632,8 +764,11 @@ export class Builder { else return obj.pack(this) as Offset } + /** + * + */ createObjectOffsetList( - list: (T extends string ? string : IGeneratedObject)[], + list: readonly (T extends string ? string : IGeneratedObject)[], ): Offset[] { const ret: number[] = [] for (let i = 0; i < list.length; ++i) { @@ -647,8 +782,11 @@ export class Builder { return ret as Offset[] } + /** + * + */ createStructOffsetList( - list: (T extends string ? string : IGeneratedObject)[], + list: readonly (T extends string ? string : IGeneratedObject)[], startFunc: (builder: Builder, length: number) => void, ): Offset { startFunc(this, list.length) @@ -657,14 +795,23 @@ export class Builder { } } +/** + * An {@link ArrayBuffer} wrapper with added utility methods. + */ export class ByteBuffer { position = 0 view: DataView + /** + * + */ constructor(buffer: ArrayBufferLike) { this.view = new DataView(buffer) } + /** + * + */ offset(bbPos: number, vtableOffset: number): Offset { const vtable = bbPos - this.view.getInt32(bbPos, true) return ( @@ -673,12 +820,18 @@ export class ByteBuffer { : 0) as Offset } + /** + * + */ union(t: Table, offset: number): Table { t.bbPos = offset + this.view.getInt32(offset, true) t.bb = this return t } + /** + * + */ rawMessage(offset: number): ArrayBuffer { offset += this.view.getInt32(offset, true) const length = this.view.getInt32(offset, true) @@ -686,20 +839,32 @@ export class ByteBuffer { return this.view.buffer.slice(offset, offset + length) } + /** + * Extract a string from the given offset. + */ message(offset: number): string { return TEXT_DECODER.decode(this.rawMessage(offset)) } + /** + * Get the offset to the start of the referenced value, given the position of the reference. + */ indirect(offset: number | Offset): Offset { return (offset + this.view.getInt32(offset, true)) as Offset } + /** + * Get the offset to the start of the vector, given the offset to the entire vector. + */ vector(offset: number | Offset): Offset { return (offset + this.view.getInt32(offset, true) + SIZEOF_INT) as Offset // data starts after the length } - vectorLength(offset: number | AnyOffset): Offset { - return this.view.getInt32(offset + this.view.getInt32(offset, true), true) as Offset + /** + * Read the length of the vector, given the offset to the entire vector. + */ + vectorLength(offset: number | AnyOffset): number { + return this.view.getInt32(offset + this.view.getInt32(offset, true), true) } } @@ -750,65 +915,108 @@ export enum ErrorPayload { export type AnyErrorPayload = None | ReadOutOfBoundsError +/** + * + */ export class InboundMessage implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): InboundMessage { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsInboundMessage(bb: ByteBuffer, obj?: InboundMessage): InboundMessage { return (obj ?? new InboundMessage()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ static getSizePrefixedRootAsInboundMessage(bb: ByteBuffer, obj?: InboundMessage): InboundMessage { bb.position += SIZE_PREFIX_LENGTH return (obj ?? new InboundMessage()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ messageId(obj?: EnsoUUID): EnsoUUID | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? (obj ?? new EnsoUUID()).init(this.bbPos + offset, this.bb!) : null } + /** + * + */ correlationId(obj?: EnsoUUID): EnsoUUID | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? (obj ?? new EnsoUUID()).init(this.bbPos + offset, this.bb!) : null } + /** + * + */ payloadType(): InboundPayload { const offset = this.bb.offset(this.bbPos, 8) return offset ? this.bb.view.getUint8(this.bbPos + offset) : InboundPayload.NONE } + /** + * + */ payload(obj: T): T | null { const offset = this.bb.offset(this.bbPos, 10) - // @ts-expect-error + // @ts-expect-error This is UNSAFE. Care must be taken to ensure `obj` is + // of the corresponding type for the `payloadType` field. return offset ? this.bb.union(obj, this.bbPos + offset) : null } + /** + * + */ static startInboundMessage(builder: Builder) { builder.startObject(4) } + /** + * + */ static addMessageId(builder: Builder, messageIdOffset: Offset) { builder.addFieldStruct(0, messageIdOffset, Null) } + /** + * + */ static addCorrelationId(builder: Builder, correlationIdOffset: Offset) { builder.addFieldStruct(1, correlationIdOffset, Null) } + /** + * + */ static addPayloadType(builder: Builder, payloadType: InboundPayload) { builder.addFieldInt8(2, payloadType, InboundPayload.NONE) } + /** + * + */ static addPayload(builder: Builder, payloadOffset: Offset) { builder.addFieldOffset(3, payloadOffset, Null) } + /** + * + */ static endInboundMessage(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // messageId @@ -816,6 +1024,9 @@ export class InboundMessage implements Table { return offset } + /** + * + */ static createInboundMessage( builder: Builder, createMessageId: CreateOffset, @@ -832,15 +1043,24 @@ export class InboundMessage implements Table { } } +/** + * + */ export class OutboundMessage implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): OutboundMessage { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsOutboundMessage(bb: ByteBuffer, obj?: OutboundMessage): OutboundMessage { return (obj ?? new OutboundMessage()).init( bb.view.getInt32(bb.position, true) + bb.position, @@ -848,6 +1068,9 @@ export class OutboundMessage implements Table { ) } + /** + * + */ static getSizePrefixedRootAsOutboundMessage( bb: ByteBuffer, obj?: OutboundMessage, @@ -859,47 +1082,78 @@ export class OutboundMessage implements Table { ) } + /** + * Get the `messageId` field of this message. + */ messageId(obj?: EnsoUUID): EnsoUUID | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? (obj ?? new EnsoUUID()).init(this.bbPos + offset, this.bb!) : null } + /** + * Get the `correlationId` field of this message. + */ correlationId(obj?: EnsoUUID): EnsoUUID | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? (obj ?? new EnsoUUID()).init(this.bbPos + offset, this.bb!) : null } + /** + * Get the `payloadType` field of this message. + */ payloadType(): OutboundPayload { const offset = this.bb.offset(this.bbPos, 8) return offset ? this.bb.view.getUint8(this.bbPos + offset) : OutboundPayload.NONE } + /** + * Get the `payload` field of this message. + */ payload(obj: T): T | null { const offset = this.bb.offset(this.bbPos, 10) - // @ts-expect-error + // @ts-expect-error This is UNSAFE. Care must be taken to ensure `obj` is + // of the corresponding type for the `payloadType` field. return offset ? this.bb.union(obj, this.bbPos + offset) : null } + /** + * Start encoding this struct in the builder. + */ static startOutboundMessage(builder: Builder) { builder.startObject(4) } + /** + * Add a `messageId` field to the given builder given its offset. + */ static addMessageId(builder: Builder, messageIdOffset: Offset) { builder.addFieldStruct(0, messageIdOffset, Null) } + /** + * Add a `correlationId` field to the given builder given its offset. + */ static addCorrelationId(builder: Builder, correlationIdOffset: Offset) { builder.addFieldStruct(1, correlationIdOffset, Null) } + /** + * Add a `payloadType` field to the given builder given its offset. + */ static addPayloadType(builder: Builder, payloadType: OutboundPayload) { builder.addFieldInt8(2, payloadType, OutboundPayload.NONE) } + /** + * Add a `payload` field to the given builder given its offset. + */ static addPayload(builder: Builder, payloadOffset: Offset) { builder.addFieldOffset(3, payloadOffset, Null) } + /** + * Finish encoding this struct in the builder. + */ static endOutboundMessage(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // messageId @@ -907,6 +1161,9 @@ export class OutboundMessage implements Table { return offset } + /** + * Encode this struct in the builder, given the values of all fields. + */ static createOutboundMessage( builder: Builder, createMessageId: CreateOffset, @@ -914,32 +1171,47 @@ export class OutboundMessage implements Table { payloadType: OutboundPayload, payloadOffset: Offset, ): Offset { - OutboundMessage.startOutboundMessage(builder) - OutboundMessage.addMessageId(builder, createMessageId?.(builder) ?? Null) - OutboundMessage.addCorrelationId(builder, createCorrelationId?.(builder) ?? Null) - OutboundMessage.addPayloadType(builder, payloadType) - OutboundMessage.addPayload(builder, payloadOffset) - return OutboundMessage.endOutboundMessage(builder) + this.startOutboundMessage(builder) + this.addMessageId(builder, createMessageId?.(builder) ?? Null) + this.addCorrelationId(builder, createCorrelationId?.(builder) ?? Null) + this.addPayloadType(builder, payloadType) + this.addPayload(builder, payloadOffset) + return this.endOutboundMessage(builder) } } +/** + * + */ export class EnsoUUID implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): EnsoUUID { this.bbPos = i this.bb = bb return this } + /** + * + */ leastSigBits(): bigint { return this.bb.view.getBigUint64(this.bbPos, true) } + /** + * + */ mostSigBits(): bigint { return this.bb.view.getBigUint64(this.bbPos + 8, true) } + /** + * + */ static createEnsoUUID( builder: Builder, leastSigBits: bigint, @@ -952,76 +1224,125 @@ export class EnsoUUID implements Table { } } +/** + * A struct representing a failed operation. + */ export class Error implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): Error { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsError(bb: ByteBuffer, obj?: Error): Error { return (obj ?? new Error()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ static getSizePrefixedRootAsError(bb: ByteBuffer, obj?: Error): Error { bb.position += SIZE_PREFIX_LENGTH return (obj ?? new Error()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * Get the `code` field of this message. + */ code(): number { const offset = this.bb.offset(this.bbPos, 4) return offset ? this.bb.view.getInt32(this.bbPos + offset, true) : 0 } + /** + * Get the `message` field of this message, as an {@link ArrayBuffer}. + */ rawMessage(): ArrayBuffer | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.rawMessage(this.bbPos + offset) : null } + /** + * Get the `message` field of this message. + */ message(): string | null { const rawMessage = this.rawMessage() return rawMessage ? TEXT_DECODER.decode(rawMessage) : null } + /** + * Get the `dataType` field of this message. + */ dataType(): ErrorPayload { const offset = this.bb.offset(this.bbPos, 8) return offset ? this.bb.view.getUint8(this.bbPos + offset) : ErrorPayload.NONE } + /** + * Get the `data` field of this message. + */ data(obj: T): T | null { const offset = this.bb.offset(this.bbPos, 10) - // @ts-expect-error + // @ts-expect-error This is UNSAFE. Care must be taken to ensure the`obj` is + // of the corresponding type for the `dataType` field. return offset ? this.bb.union(obj, this.bbPos + offset) : null } + /** + * + */ static startError(builder: Builder) { builder.startObject(4) } + /** + * + */ static addCode(builder: Builder, code: number) { builder.addFieldInt32(0, code, 0) } + /** + * + */ static addMessage(builder: Builder, messageOffset: Offset) { builder.addFieldOffset(1, messageOffset, Null) } + /** + * + */ static addDataType(builder: Builder, dataType: ErrorPayload) { builder.addFieldInt8(2, dataType, ErrorPayload.NONE) } + /** + * + */ static addData(builder: Builder, dataOffset: Offset) { builder.addFieldOffset(3, dataOffset, Null) } + /** + * + */ static endError(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 6) // message return offset } + /** + * + */ static createError( builder: Builder, code: number, @@ -1038,15 +1359,24 @@ export class Error implements Table { } } +/** + * + */ export class ReadOutOfBoundsError implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): ReadOutOfBoundsError { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsReadOutOfBoundsError( bb: ByteBuffer, obj?: ReadOutOfBoundsError, @@ -1057,6 +1387,9 @@ export class ReadOutOfBoundsError implements Table { ) } + /** + * + */ static getSizePrefixedRootAsReadOutOfBoundsError( bb: ByteBuffer, obj?: ReadOutOfBoundsError, @@ -1068,24 +1401,39 @@ export class ReadOutOfBoundsError implements Table { ) } + /** + * + */ fileLength(): bigint { const offset = this.bb.offset(this.bbPos, 4) return offset ? this.bb.view.getBigUint64(this.bbPos + offset, true) : 0n } + /** + * + */ static startReadOutOfBoundsError(builder: Builder) { builder.startObject(1) } + /** + * + */ static addFileLength(builder: Builder, fileLength: bigint) { builder.addFieldInt64(0, fileLength, 0n) } + /** + * + */ static endReadOutOfBoundsError(builder: Builder): Offset { const offset = builder.endObject() return offset } + /** + * + */ static createReadOutOfBoundsError( builder: Builder, fileLength: bigint, @@ -1096,9 +1444,15 @@ export class ReadOutOfBoundsError implements Table { } } +/** + * + */ export class None implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): None { this.bbPos = i this.bb = bb @@ -1106,48 +1460,78 @@ export class None implements Table { } } +/** + * + */ export class Success implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): Success { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsSuccess(bb: ByteBuffer, obj?: Success): Success { return (obj ?? new Success()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ static getSizePrefixedRootAsSuccess(bb: ByteBuffer, obj?: Success): Success { bb.position += SIZE_PREFIX_LENGTH return (obj ?? new Success()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ static startSuccess(builder: Builder) { builder.startObject(0) } + /** + * + */ static endSuccess(builder: Builder): Offset { const offset = builder.endObject() return offset } + /** + * + */ static createSuccess(builder: Builder): Offset { Success.startSuccess(builder) return Success.endSuccess(builder) } } +/** + * + */ export class InitSessionCommand implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): InitSessionCommand { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsInitSessionCommand(bb: ByteBuffer, obj?: InitSessionCommand): InitSessionCommand { return (obj ?? new InitSessionCommand()).init( bb.view.getInt32(bb.position, true) + bb.position, @@ -1155,6 +1539,9 @@ export class InitSessionCommand implements Table { ) } + /** + * + */ static getSizePrefixedRootAsInitSessionCommand( bb: ByteBuffer, obj?: InitSessionCommand, @@ -1166,25 +1553,40 @@ export class InitSessionCommand implements Table { ) } + /** + * + */ identifier(obj?: EnsoUUID): EnsoUUID | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? (obj ?? new EnsoUUID()).init(this.bbPos + offset, this.bb!) : null } + /** + * + */ static startInitSessionCommand(builder: Builder) { builder.startObject(1) } + /** + * + */ static addIdentifier(builder: Builder, identifierOffset: Offset) { builder.addFieldStruct(0, identifierOffset, Null) } + /** + * + */ static endInitSessionCommand(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // identifier return offset } + /** + * + */ static createInitSessionCommand( builder: Builder, createIdentifier: CreateOffset, @@ -1195,15 +1597,24 @@ export class InitSessionCommand implements Table { } } +/** + * + */ export class VisualizationContext implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): VisualizationContext { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsVisualizationContext( bb: ByteBuffer, obj?: VisualizationContext, @@ -1214,6 +1625,9 @@ export class VisualizationContext implements Table { ) } + /** + * + */ static getSizePrefixedRootAsVisualizationContext( bb: ByteBuffer, obj?: VisualizationContext, @@ -1225,37 +1639,61 @@ export class VisualizationContext implements Table { ) } + /** + * + */ visualizationId(obj?: EnsoUUID): EnsoUUID | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? (obj ?? new EnsoUUID()).init(this.bbPos + offset, this.bb!) : null } + /** + * + */ contextId(obj?: EnsoUUID): EnsoUUID | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? (obj ?? new EnsoUUID()).init(this.bbPos + offset, this.bb!) : null } + /** + * + */ expressionId(obj?: EnsoUUID): EnsoUUID | null { const offset = this.bb.offset(this.bbPos, 8) return offset ? (obj ?? new EnsoUUID()).init(this.bbPos + offset, this.bb!) : null } + /** + * + */ static startVisualizationContext(builder: Builder) { builder.startObject(3) } + /** + * + */ static addVisualizationId(builder: Builder, visualizationIdOffset: Offset) { builder.addFieldStruct(0, visualizationIdOffset, Null) } + /** + * + */ static addContextId(builder: Builder, contextIdOffset: Offset) { builder.addFieldStruct(1, contextIdOffset, Null) } + /** + * + */ static addExpressionId(builder: Builder, expressionIdOffset: Offset) { builder.addFieldStruct(2, expressionIdOffset, Null) } + /** + * + */ static endVisualizationContext(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // visualizationId @@ -1264,6 +1702,9 @@ export class VisualizationContext implements Table { return offset } + /** + * + */ static createVisualizationContext( builder: Builder, createVisualizationId: CreateOffset, @@ -1278,15 +1719,24 @@ export class VisualizationContext implements Table { } } +/** + * + */ export class VisualizationUpdate implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): VisualizationUpdate { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsVisualizationUpdate( bb: ByteBuffer, obj?: VisualizationUpdate, @@ -1297,6 +1747,9 @@ export class VisualizationUpdate implements Table { ) } + /** + * + */ static getSizePrefixedRootAsVisualizationUpdate( bb: ByteBuffer, obj?: VisualizationUpdate, @@ -1308,6 +1761,9 @@ export class VisualizationUpdate implements Table { ) } + /** + * + */ visualizationContext(obj?: VisualizationContext): VisualizationContext | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? @@ -1315,16 +1771,25 @@ export class VisualizationUpdate implements Table { : null } + /** + * + */ data(index: number): number | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.view.getUint8(this.bb.vector(this.bbPos + offset) + index) : 0 } + /** + * + */ dataLength(): number { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.vectorLength(this.bbPos + offset) : 0 } + /** + * + */ dataArray(): Uint8Array | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? @@ -1336,15 +1801,24 @@ export class VisualizationUpdate implements Table { : null } + /** + * + */ dataString(): string | null { const buffer = this.dataArray() return buffer != null ? TEXT_DECODER.decode(buffer) : null } + /** + * + */ static startVisualizationUpdate(builder: Builder) { builder.startObject(2) } + /** + * + */ static addVisualizationContext( builder: Builder, visualizationContextOffset: Offset, @@ -1352,10 +1826,16 @@ export class VisualizationUpdate implements Table { builder.addFieldOffset(0, visualizationContextOffset, Null) } + /** + * + */ static addData(builder: Builder, dataOffset: Offset) { builder.addFieldOffset(1, dataOffset, Null) } + /** + * + */ static createDataVector(builder: Builder, data: number[] | Uint8Array): Offset { builder.startVector(1, data.length, 1) // An iterator is more type-safe, but less performant. @@ -1365,10 +1845,16 @@ export class VisualizationUpdate implements Table { return builder.endVector() } + /** + * + */ static startDataVector(builder: Builder, numElems: number) { builder.startVector(1, numElems, 1) } + /** + * + */ static endVisualizationUpdate(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // visualizationContext @@ -1376,6 +1862,9 @@ export class VisualizationUpdate implements Table { return offset } + /** + * + */ static createVisualizationUpdate( builder: Builder, visualizationContextOffset: Offset, @@ -1388,29 +1877,47 @@ export class VisualizationUpdate implements Table { } } +/** + * + */ export class Path implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): Path { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsPath(bb: ByteBuffer, obj?: Path): Path { return (obj ?? new Path()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ static getSizePrefixedRootAsPath(bb: ByteBuffer, obj?: Path): Path { bb.position += SIZE_PREFIX_LENGTH return (obj ?? new Path()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ rootId(obj?: EnsoUUID): EnsoUUID | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? (obj ?? new EnsoUUID()).init(this.bbPos + offset, this.bb) : null } + /** + * + */ rawSegments(index: number): ArrayBuffer { const offset = this.bb.offset(this.bbPos, 6) return offset ? @@ -1418,27 +1925,45 @@ export class Path implements Table { : new Uint8Array() } + /** + * + */ segments(index: number): string { return TEXT_DECODER.decode(this.rawSegments(index)) } + /** + * + */ segmentsLength(): number { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.vectorLength(this.bbPos + offset) : 0 } + /** + * + */ static startPath(builder: Builder) { builder.startObject(2) } + /** + * + */ static addRootId(builder: Builder, rootIdOffset: Offset) { builder.addFieldStruct(0, rootIdOffset, Null) } + /** + * + */ static addSegments(builder: Builder, segmentsOffset: Offset) { builder.addFieldOffset(1, segmentsOffset, Null) } + /** + * + */ static createSegmentsVector( builder: Builder, data: Offset[] | Offset[], @@ -1451,15 +1976,24 @@ export class Path implements Table { return builder.endVector() } + /** + * + */ static startSegmentsVector(builder: Builder, numElems: number) { builder.startVector(4, numElems, 4) } + /** + * + */ static endPath(builder: Builder): Offset { const offset = builder.endObject() return offset } + /** + * + */ static createPath( builder: Builder, createRootId: CreateOffset, @@ -1472,15 +2006,24 @@ export class Path implements Table { } } +/** + * + */ export class WriteFileCommand implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): WriteFileCommand { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsWriteFileCommand(bb: ByteBuffer, obj?: WriteFileCommand): WriteFileCommand { return (obj ?? new WriteFileCommand()).init( bb.view.getInt32(bb.position, true) + bb.position, @@ -1488,6 +2031,9 @@ export class WriteFileCommand implements Table { ) } + /** + * + */ static getSizePrefixedRootAsWriteFileCommand( bb: ByteBuffer, obj?: WriteFileCommand, @@ -1499,21 +2045,33 @@ export class WriteFileCommand implements Table { ) } + /** + * + */ path(obj?: Path): Path | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? (obj ?? new Path()).init(this.bb.indirect(this.bbPos + offset), this.bb!) : null } + /** + * + */ contents(index: number): number | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.view.getUint8(this.bb.vector(this.bbPos + offset) + index) : 0 } + /** + * + */ contentsLength(): number { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.vectorLength(this.bbPos + offset) : 0 } + /** + * + */ contentsArray(): Uint8Array | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? @@ -1525,18 +2083,30 @@ export class WriteFileCommand implements Table { : null } + /** + * + */ static startWriteFileCommand(builder: Builder) { builder.startObject(2) } + /** + * + */ static addPath(builder: Builder, pathOffset: Offset) { builder.addFieldOffset(0, pathOffset, Null) } + /** + * + */ static addContents(builder: Builder, contentsOffset: Offset) { builder.addFieldOffset(1, contentsOffset, Null) } + /** + * + */ static createContentsVector(builder: Builder, data: number[] | Uint8Array): Offset { builder.startVector(1, data.length, 1) // An iterator is more type-safe, but less performant. @@ -1546,15 +2116,24 @@ export class WriteFileCommand implements Table { return builder.endVector() } + /** + * + */ static startContentsVector(builder: Builder, numElems: number) { builder.startVector(1, numElems, 1) } + /** + * + */ static endWriteFileCommand(builder: Builder): Offset { const offset = builder.endObject() return offset } + /** + * + */ static createWriteFileCommand( builder: Builder, pathOffset: Offset, @@ -1567,15 +2146,24 @@ export class WriteFileCommand implements Table { } } +/** + * + */ export class ReadFileCommand implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): ReadFileCommand { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsReadFileCommand(bb: ByteBuffer, obj?: ReadFileCommand): ReadFileCommand { return (obj ?? new ReadFileCommand()).init( bb.view.getInt32(bb.position, true) + bb.position, @@ -1583,6 +2171,9 @@ export class ReadFileCommand implements Table { ) } + /** + * + */ static getSizePrefixedRootAsReadFileCommand( bb: ByteBuffer, obj?: ReadFileCommand, @@ -1594,24 +2185,39 @@ export class ReadFileCommand implements Table { ) } + /** + * + */ path(obj?: Path): Path | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? (obj ?? new Path()).init(this.bb.indirect(this.bbPos + offset), this.bb!) : null } - static startReadFileCommand(builder: Builder) { - builder.startObject(1) + /** + * + */ + static startReadFileCommand(builder: Builder) { + builder.startObject(1) } + /** + * + */ static addPath(builder: Builder, pathOffset: Offset) { builder.addFieldOffset(0, pathOffset, Null) } + /** + * + */ static endReadFileCommand(builder: Builder): Offset { const offset = builder.endObject() return offset } + /** + * + */ static createReadFileCommand( builder: Builder, pathOffset: Offset, @@ -1622,15 +2228,24 @@ export class ReadFileCommand implements Table { } } +/** + * + */ export class FileContentsReply implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): FileContentsReply { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsFileContentsReply(bb: ByteBuffer, obj?: FileContentsReply): FileContentsReply { return (obj ?? new FileContentsReply()).init( bb.view.getInt32(bb.position, true) + bb.position, @@ -1638,6 +2253,9 @@ export class FileContentsReply implements Table { ) } + /** + * + */ static getSizePrefixedRootAsFileContentsReply( bb: ByteBuffer, obj?: FileContentsReply, @@ -1649,16 +2267,25 @@ export class FileContentsReply implements Table { ) } + /** + * + */ contents(index: number): number | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? this.bb.view.getUint8(this.bb.vector(this.bbPos + offset) + index) : 0 } + /** + * + */ contentsLength(): number { const offset = this.bb.offset(this.bbPos, 4) return offset ? this.bb.vectorLength(this.bbPos + offset) : 0 } + /** + * + */ contentsArray(): Uint8Array | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? @@ -1670,14 +2297,23 @@ export class FileContentsReply implements Table { : null } + /** + * + */ static startFileContentsReply(builder: Builder) { builder.startObject(1) } + /** + * + */ static addContents(builder: Builder, contentsOffset: Offset) { builder.addFieldOffset(0, contentsOffset, Null) } + /** + * + */ static createContentsVector(builder: Builder, data: number[] | Uint8Array): Offset { builder.startVector(1, data.length, 1) // An iterator is more type-safe, but less performant. @@ -1687,15 +2323,24 @@ export class FileContentsReply implements Table { return builder.endVector() } + /** + * + */ static startContentsVector(builder: Builder, numElems: number) { builder.startVector(1, numElems, 1) } + /** + * + */ static endFileContentsReply(builder: Builder): Offset { const offset = builder.endObject() return offset } + /** + * + */ static createFileContentsReply( builder: Builder, contentsOffset: Offset, @@ -1706,15 +2351,24 @@ export class FileContentsReply implements Table { } } +/** + * + */ export class WriteBytesCommand implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): WriteBytesCommand { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsWriteBytesCommand(bb: ByteBuffer, obj?: WriteBytesCommand): WriteBytesCommand { return (obj ?? new WriteBytesCommand()).init( bb.view.getInt32(bb.position, true) + bb.position, @@ -1722,6 +2376,9 @@ export class WriteBytesCommand implements Table { ) } + /** + * + */ static getSizePrefixedRootAsWriteBytesCommand( bb: ByteBuffer, obj?: WriteBytesCommand, @@ -1733,31 +2390,49 @@ export class WriteBytesCommand implements Table { ) } + /** + * + */ path(obj?: Path): Path | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? (obj ?? new Path()).init(this.bb.indirect(this.bbPos + offset), this.bb!) : null } + /** + * + */ byteOffset(): bigint { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.view.getBigUint64(this.bbPos + offset, true) : 0n } + /** + * + */ overwriteExisting(): boolean { const offset = this.bb.offset(this.bbPos, 8) return offset ? !!this.bb.view.getInt8(this.bbPos + offset) : false } + /** + * + */ bytes(index: number): number | null { const offset = this.bb.offset(this.bbPos, 10) return offset ? this.bb.view.getUint8(this.bb.vector(this.bbPos + offset) + index) : 0 } + /** + * + */ bytesLength(): number { const offset = this.bb.offset(this.bbPos, 10) return offset ? this.bb.vectorLength(this.bbPos + offset) : 0 } + /** + * + */ bytesArray(): Uint8Array | null { const offset = this.bb.offset(this.bbPos, 10) return offset ? @@ -1769,26 +2444,44 @@ export class WriteBytesCommand implements Table { : null } + /** + * + */ static startWriteBytesCommand(builder: Builder) { builder.startObject(4) } + /** + * + */ static addPath(builder: Builder, pathOffset: Offset) { builder.addFieldOffset(0, pathOffset, Null) } + /** + * + */ static addByteOffset(builder: Builder, byteOffset: bigint) { builder.addFieldInt64(1, byteOffset, 0n) } + /** + * + */ static addOverwriteExisting(builder: Builder, overwriteExisting: boolean) { builder.addFieldInt8(2, +overwriteExisting, +false) } + /** + * + */ static addBytes(builder: Builder, bytesOffset: Offset) { builder.addFieldOffset(3, bytesOffset, Null) } + /** + * + */ static createBytesVector(builder: Builder, data: number[] | Uint8Array): Offset { builder.startVector(1, data.length, 1) // An iterator is more type-safe, but less performant. @@ -1798,10 +2491,16 @@ export class WriteBytesCommand implements Table { return builder.endVector() } + /** + * + */ static startBytesVector(builder: Builder, numElems: number) { builder.startVector(1, numElems, 1) } + /** + * + */ static endWriteBytesCommand(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // path @@ -1809,6 +2508,9 @@ export class WriteBytesCommand implements Table { return offset } + /** + * + */ static createWriteBytesCommand( builder: Builder, pathOffset: Offset, @@ -1825,15 +2527,24 @@ export class WriteBytesCommand implements Table { } } +/** + * + */ export class WriteBytesReply implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): WriteBytesReply { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsWriteBytesReply(bb: ByteBuffer, obj?: WriteBytesReply): WriteBytesReply { return (obj ?? new WriteBytesReply()).init( bb.view.getInt32(bb.position, true) + bb.position, @@ -1841,6 +2552,9 @@ export class WriteBytesReply implements Table { ) } + /** + * + */ static getSizePrefixedRootAsWriteBytesReply( bb: ByteBuffer, obj?: WriteBytesReply, @@ -1852,6 +2566,9 @@ export class WriteBytesReply implements Table { ) } + /** + * + */ checksum(obj?: EnsoDigest): EnsoDigest | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? @@ -1859,20 +2576,32 @@ export class WriteBytesReply implements Table { : null } + /** + * + */ static startWriteBytesReply(builder: Builder) { builder.startObject(1) } + /** + * + */ static addChecksum(builder: Builder, checksumOffset: Offset) { builder.addFieldOffset(0, checksumOffset, Null) } + /** + * + */ static endWriteBytesReply(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // checksum return offset } + /** + * + */ static createWriteBytesReply( builder: Builder, checksumOffset: Offset, @@ -1883,15 +2612,24 @@ export class WriteBytesReply implements Table { } } +/** + * + */ export class ReadBytesCommand implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): ReadBytesCommand { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsReadBytesCommand(bb: ByteBuffer, obj?: ReadBytesCommand): ReadBytesCommand { return (obj ?? new ReadBytesCommand()).init( bb.view.getInt32(bb.position, true) + bb.position, @@ -1899,6 +2637,9 @@ export class ReadBytesCommand implements Table { ) } + /** + * + */ static getSizePrefixedRootAsReadBytesCommand( bb: ByteBuffer, obj?: ReadBytesCommand, @@ -1910,6 +2651,9 @@ export class ReadBytesCommand implements Table { ) } + /** + * + */ segment(obj?: FileSegment): FileSegment | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? @@ -1917,20 +2661,32 @@ export class ReadBytesCommand implements Table { : null } + /** + * + */ static startReadBytesCommand(builder: Builder) { builder.startObject(1) } + /** + * + */ static addSegment(builder: Builder, segmentOffset: Offset) { builder.addFieldOffset(0, segmentOffset, Null) } + /** + * + */ static endReadBytesCommand(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // segment return offset } + /** + * + */ static createReadBytesCommand( builder: Builder, segmentOffset: Offset, @@ -1941,24 +2697,39 @@ export class ReadBytesCommand implements Table { } } +/** + * + */ export class ReadBytesReply implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): ReadBytesReply { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsReadBytesReply(bb: ByteBuffer, obj?: ReadBytesReply): ReadBytesReply { return (obj ?? new ReadBytesReply()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ static getSizePrefixedRootAsReadBytesReply(bb: ByteBuffer, obj?: ReadBytesReply): ReadBytesReply { bb.position += SIZE_PREFIX_LENGTH return (obj ?? new ReadBytesReply()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ checksum(obj?: EnsoDigest): EnsoDigest | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? @@ -1966,16 +2737,25 @@ export class ReadBytesReply implements Table { : null } + /** + * + */ bytes(index: number): number | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.view.getUint8(this.bb.vector(this.bbPos + offset) + index) : 0 } + /** + * + */ bytesLength(): number { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.vectorLength(this.bbPos + offset) : 0 } + /** + * + */ bytesArray(): Uint8Array | null { const offset = this.bb.offset(this.bbPos, 6) return offset ? @@ -1987,18 +2767,30 @@ export class ReadBytesReply implements Table { : null } + /** + * + */ static startReadBytesReply(builder: Builder) { builder.startObject(2) } + /** + * + */ static addChecksum(builder: Builder, checksumOffset: Offset) { builder.addFieldOffset(0, checksumOffset, Null) } + /** + * + */ static addBytes(builder: Builder, bytesOffset: Offset) { builder.addFieldOffset(1, bytesOffset, Null) } + /** + * + */ static createBytesVector(builder: Builder, data: number[] | Uint8Array): Offset { builder.startVector(1, data.length, 1) // An iterator is more type-safe, but less performant. @@ -2008,10 +2800,16 @@ export class ReadBytesReply implements Table { return builder.endVector() } + /** + * + */ static startBytesVector(builder: Builder, numElems: number) { builder.startVector(1, numElems, 1) } + /** + * + */ static endReadBytesReply(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // checksum @@ -2019,6 +2817,9 @@ export class ReadBytesReply implements Table { return offset } + /** + * + */ static createReadBytesReply( builder: Builder, checksumOffset: Offset, @@ -2031,15 +2832,24 @@ export class ReadBytesReply implements Table { } } +/** + * + */ export class ChecksumBytesCommand implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): ChecksumBytesCommand { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsChecksumBytesCommand( bb: ByteBuffer, obj?: ChecksumBytesCommand, @@ -2050,6 +2860,9 @@ export class ChecksumBytesCommand implements Table { ) } + /** + * + */ static getSizePrefixedRootAsChecksumBytesCommand( bb: ByteBuffer, obj?: ChecksumBytesCommand, @@ -2061,6 +2874,9 @@ export class ChecksumBytesCommand implements Table { ) } + /** + * + */ segment(obj?: FileSegment): FileSegment | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? @@ -2068,20 +2884,32 @@ export class ChecksumBytesCommand implements Table { : null } + /** + * + */ static startChecksumBytesCommand(builder: Builder) { builder.startObject(1) } + /** + * + */ static addSegment(builder: Builder, segmentOffset: Offset) { builder.addFieldOffset(0, segmentOffset, Null) } + /** + * + */ static endChecksumBytesCommand(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // segment return offset } + /** + * + */ static createChecksumBytesCommand( builder: Builder, segmentOffset: Offset, @@ -2092,15 +2920,24 @@ export class ChecksumBytesCommand implements Table { } } +/** + * + */ export class ChecksumBytesReply implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): ChecksumBytesReply { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsChecksumBytesReply(bb: ByteBuffer, obj?: ChecksumBytesReply): ChecksumBytesReply { return (obj ?? new ChecksumBytesReply()).init( bb.view.getInt32(bb.position, true) + bb.position, @@ -2108,6 +2945,9 @@ export class ChecksumBytesReply implements Table { ) } + /** + * + */ static getSizePrefixedRootAsChecksumBytesReply( bb: ByteBuffer, obj?: ChecksumBytesReply, @@ -2119,6 +2959,9 @@ export class ChecksumBytesReply implements Table { ) } + /** + * + */ checksum(obj?: EnsoDigest): EnsoDigest | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? @@ -2126,20 +2969,32 @@ export class ChecksumBytesReply implements Table { : null } + /** + * + */ static startChecksumBytesReply(builder: Builder) { builder.startObject(1) } + /** + * + */ static addChecksum(builder: Builder, checksumOffset: Offset) { builder.addFieldOffset(0, checksumOffset, Null) } + /** + * + */ static endChecksumBytesReply(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // checksum return offset } + /** + * + */ static createChecksumBytesReply( builder: Builder, checksumOffset: Offset, @@ -2150,34 +3005,55 @@ export class ChecksumBytesReply implements Table { } } +/** + * + */ export class EnsoDigest implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): EnsoDigest { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsEnsoDigest(bb: ByteBuffer, obj?: EnsoDigest): EnsoDigest { return (obj ?? new EnsoDigest()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ static getSizePrefixedRootAsEnsoDigest(bb: ByteBuffer, obj?: EnsoDigest): EnsoDigest { bb.position += SIZE_PREFIX_LENGTH return (obj ?? new EnsoDigest()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ bytes(index: number): number | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? this.bb.view.getUint8(this.bb.vector(this.bbPos + offset) + index) : 0 } + /** + * + */ bytesLength(): number { const offset = this.bb.offset(this.bbPos, 4) return offset ? this.bb.vectorLength(this.bbPos + offset) : 0 } + /** + * + */ bytesArray(): Uint8Array | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? @@ -2189,14 +3065,23 @@ export class EnsoDigest implements Table { : null } + /** + * + */ static startEnsoDigest(builder: Builder) { builder.startObject(1) } + /** + * + */ static addBytes(builder: Builder, bytesOffset: Offset) { builder.addFieldOffset(0, bytesOffset, Null) } + /** + * + */ static createBytesVector(builder: Builder, data: number[] | Uint8Array): Offset { builder.startVector(1, data.length, 1) // An iterator is more type-safe, but less performant. @@ -2206,16 +3091,25 @@ export class EnsoDigest implements Table { return builder.endVector() } + /** + * + */ static startBytesVector(builder: Builder, numElems: number) { builder.startVector(1, numElems, 1) } + /** + * + */ static endEnsoDigest(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // bytes return offset } + /** + * + */ static createEnsoDigest(builder: Builder, bytesOffset: Offset): Offset { EnsoDigest.startEnsoDigest(builder) EnsoDigest.addBytes(builder, bytesOffset) @@ -2223,61 +3117,100 @@ export class EnsoDigest implements Table { } } +/** + * + */ export class FileSegment implements Table { bb!: ByteBuffer bbPos: number = 0 + /** + * + */ init(i: number, bb: ByteBuffer): FileSegment { this.bbPos = i this.bb = bb return this } + /** + * + */ static getRootAsFileSegment(bb: ByteBuffer, obj?: FileSegment): FileSegment { return (obj ?? new FileSegment()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ static getSizePrefixedRootAsFileSegment(bb: ByteBuffer, obj?: FileSegment): FileSegment { bb.position += SIZE_PREFIX_LENGTH return (obj ?? new FileSegment()).init(bb.view.getInt32(bb.position, true) + bb.position, bb) } + /** + * + */ path(obj?: Path): Path | null { const offset = this.bb.offset(this.bbPos, 4) return offset ? (obj ?? new Path()).init(this.bb.indirect(this.bbPos + offset), this.bb!) : null } + /** + * + */ byteOffset(): bigint { const offset = this.bb.offset(this.bbPos, 6) return offset ? this.bb.view.getBigUint64(this.bbPos + offset, true) : 0n } + /** + * + */ length(): bigint { const offset = this.bb.offset(this.bbPos, 8) return offset ? this.bb.view.getBigUint64(this.bbPos + offset, true) : 0n } + /** + * + */ static startFileSegment(builder: Builder) { builder.startObject(3) } + /** + * + */ static addPath(builder: Builder, pathOffset: Offset) { builder.addFieldOffset(0, pathOffset, Null) } + /** + * + */ static addByteOffset(builder: Builder, byteOffset: bigint) { builder.addFieldInt64(1, byteOffset, 0n) } + /** + * + */ static addLength(builder: Builder, length: bigint) { builder.addFieldInt64(2, length, 0n) } + /** + * + */ static endFileSegment(builder: Builder): Offset { const offset = builder.endObject() builder.requiredField(offset, 4) // path return offset } + /** + * + */ static createFileSegment( builder: Builder, pathOffset: Offset, diff --git a/app/ydoc-shared/src/ensoFile.ts b/app/ydoc-shared/src/ensoFile.ts index d5703f3950e3..92aace11fdec 100644 --- a/app/ydoc-shared/src/ensoFile.ts +++ b/app/ydoc-shared/src/ensoFile.ts @@ -6,6 +6,9 @@ export interface EnsoFileParts { metadataJson: string | null } +/** + * Return the parts of a file, given its entire content. + */ export function splitFileContents(content: string): EnsoFileParts { const splitPoint = content.lastIndexOf(META_TAG) if (splitPoint < 0) { @@ -23,6 +26,9 @@ export function splitFileContents(content: string): EnsoFileParts { return { code, idMapJson, metadataJson } } +/** + * Return the entire content of a file, given its parts. + */ export function combineFileParts(parts: EnsoFileParts): string { const hasMeta = parts.idMapJson != null || parts.metadataJson != null if (hasMeta) { diff --git a/app/ydoc-shared/src/languageServer.ts b/app/ydoc-shared/src/languageServer.ts index e1403f504202..faae05b41f25 100644 --- a/app/ydoc-shared/src/languageServer.ts +++ b/app/ydoc-shared/src/languageServer.ts @@ -87,10 +87,16 @@ const RemoteRpcErrorSchema = z.object({ }) type RemoteRpcErrorParsed = z.infer +/** + * Payload for a {@linnk LsRpcError}. + */ export class RemoteRpcError { code: ErrorCode message: string data?: any + /** + * Create a {@link RemoteRpcError}. + */ constructor(error: RemoteRpcErrorParsed) { this.code = error.code this.message = error.message @@ -98,16 +104,25 @@ export class RemoteRpcError { } } +/** + * An error executing a request from the {@link LanguageServer}. + */ export class LsRpcError { cause: RemoteRpcError | Error | string request: string params: object + /** + * Create an {@link LsRpcError}. + */ constructor(cause: RemoteRpcError | Error | string, request: string, params: object) { this.cause = cause this.request = request this.params = params } + /** + * Get a human-readable string representation of this error. + */ toString() { return `Language Server request '${this.request}' failed: ${this.cause instanceof RemoteRpcError ? this.cause.message : this.cause}` } @@ -140,6 +155,9 @@ export class LanguageServer extends ObservableV2 { return this.initialized.then(result => (result.ok ? result.value.contentRoots : [])) } + /** + * Reconnect the underlying network transport. + */ reconnect() { this.transport.reconnect() } @@ -259,6 +286,7 @@ export class LanguageServer extends ObservableV2> { return this.acquireCapability('executionContext/canModify', { contextId }) } @@ -287,7 +315,7 @@ export class LanguageServer extends ObservableV2> { return this.request('text/applyEdit', { edit, execute, idMap }) } @@ -512,15 +540,19 @@ export class LanguageServer extends ObservableV2> { return this.request('ai/completion', { prompt, stopSequence }) } - /** A helper function to subscribe to file updates. + /** + * A helper function to subscribe to file updates. * Please use `ls.on('file/event')` directly if the initial `'Added'` notifications are not - * needed. */ + * needed. + */ watchFiles(rootId: Uuid, segments: string[], callback: (event: Event<'file/event'>) => void) { let running = true + // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this return { promise: (async () => { @@ -550,6 +582,10 @@ export class LanguageServer extends ObservableV2 0) { this.retainCount -= 1 @@ -581,6 +624,9 @@ export class LanguageServer extends ObservableV2 { contents: T } -export interface TextFileContents extends FileContents {} +export type TextFileContents = FileContents export interface DirectoryTree { path: Path @@ -83,7 +83,7 @@ export type IdMapTuple = [IdMapSpan, string] export type IdMapTriple = [number, number, string] -export type RegisterOptions = { path: Path } | { contextId: ContextId } | {} +export type RegisterOptions = { path: Path } | { contextId: ContextId } | object export interface CapabilityRegistration { method: string @@ -163,9 +163,11 @@ export interface Pending { export interface Warnings { /** The number of attached warnings. */ count: number - /** If the value has a single warning attached, this field contains textual + /** + * If the value has a single warning attached, this field contains textual * representation of the attached warning. In general, warning values should - * be obtained by attaching an appropriate visualization to a value. */ + * be obtained by attaching an appropriate visualization to a value. + */ value?: string } @@ -189,6 +191,9 @@ export interface MethodPointer { name: string } +/** + * Whether one {@link MethodPointer} deeply equals another. + */ export function methodPointerEquals(left: MethodPointer, right: MethodPointer): boolean { return ( left.module === right.module && @@ -251,8 +256,10 @@ export type FileSystemObject = name: string path: Path } - /** A directory which contents have been truncated, i.e. with its subtree not listed any further - * due to depth limit being reached. */ + /** + * A directory which contents have been truncated, i.e. with its subtree not listed any further + * due to depth limit being reached. + */ | { type: 'DirectoryTruncated' name: string @@ -278,8 +285,10 @@ export type FileSystemObject = target: Path } -/** A single component of a component group. - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarycomponent) */ +/** + * A single component of a component group. + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarycomponent) + */ export interface LibraryComponent { /** The component name. */ name: string @@ -287,8 +296,10 @@ export interface LibraryComponent { shortcut?: string } -/** The component group provided by a library. - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarycomponentgroup) */ +/** + * The component group provided by a library. + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarycomponentgroup) + */ export interface LibraryComponentGroup { /** * The fully qualified library name. A string consisting of a namespace and @@ -296,7 +307,8 @@ export interface LibraryComponentGroup { * i.e. `Standard.Base`. */ library: string - /** The group name without the library name prefix. + /** + * The group name without the library name prefix. * E.g. given the `Standard.Base.Group 1` group reference, * the `name` field contains `Group 1`. */ @@ -310,8 +322,10 @@ export interface LibraryComponentGroup { export interface VisualizationConfiguration { /** An execution context of the visualization. */ executionContextId: ContextId - /** A qualified name of the module to be used to evaluate the arguments for the visualization - * expression. */ + /** + * A qualified name of the module to be used to evaluate the arguments for the visualization + * expression. + */ visualizationModule: string /** An expression that creates a visualization. */ expression: string | MethodPointer @@ -350,8 +364,8 @@ export type Notifications = { currentVersion: number }) => void 'file/event': (param: { path: Path; kind: FileEventKind }) => void - 'file/rootAdded': (param: {}) => void - 'file/rootRemoved': (param: {}) => void + 'file/rootAdded': (param: object) => void + 'file/rootRemoved': (param: object) => void 'refactoring/projectRenamed': (param: { oldNormalizedName: string newNormalizedName: string @@ -377,12 +391,18 @@ export interface LocalCall { expressionId: ExpressionId } +/** + * Serialize a {@link MethodPointer}. + */ export function encodeMethodPointer(enc: encoding.Encoder, ptr: MethodPointer) { encoding.writeVarString(enc, ptr.module) encoding.writeVarString(enc, ptr.name) encoding.writeVarString(enc, ptr.definedOnType) } +/** + * Whether one {@link StackItem} is deeply equal to another. + */ export function stackItemsEqual(left: StackItem, right: StackItem): boolean { if (left.type !== right.type) return false @@ -406,7 +426,7 @@ export namespace response { contentRoots: ContentRoot[] } - export interface FileContents extends TextFileContents {} + export type FileContents = TextFileContents export interface FileExists { exists: boolean @@ -480,9 +500,11 @@ export interface LanguageServerError { export enum LanguageServerErrorCode { // === Error API errors === // https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/ErrorApi.scala - /** The user doesn't have access to the requested resource. + /** + * The user doesn't have access to the requested resource. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#accessdeniederror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#accessdeniederror) + */ AccessDenied = 100, // === VCS Manager API errors === @@ -501,208 +523,296 @@ export enum LanguageServerErrorCode { // === File Manager API errors === // https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerApi.scala - /** A miscellaneous file system error. + /** + * A miscellaneous file system error. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filesystemerror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filesystemerror) + */ + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values FileSystem = 1000, - /** The requested content root could not be found. + /** + * The requested content root could not be found. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#contentrootnotfounderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#contentrootnotfounderror) + */ ContentRootNotFound = 1001, - /** The requested file does not exist. + /** + * The requested file does not exist. * - *[Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filenotfound) */ + *[Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filenotfound) + */ + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values FileNotFound = 1003, - /** The file trying to be created already exists. + /** + * The file trying to be created already exists. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#fileexists) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#fileexists) + */ + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values FileExists = 1004, - /** The IO operation timed out. + /** + * The IO operation timed out. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#operationtimeouterror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#operationtimeouterror) + */ + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values OperationTimeoutError = 1005, - /** The provided path is not a directory. + /** + * The provided path is not a directory. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#notdirectory) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#notdirectory) + */ NotDirectory = 1006, - /** The provided path is not a file. + /** + * The provided path is not a file. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#notfile) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#notfile) + */ NotFile = 1007, - /** The streaming file write cannot overwrite a portion of the requested file. + /** + * The streaming file write cannot overwrite a portion of the requested file. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#cannotoverwrite) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#cannotoverwrite) + */ CannotOverwrite = 1008, - /** The requested file read was out of bounds for the file's size. + /** + * The requested file read was out of bounds for the file's size. * * The actual length of the file is returned in `payload.fileLength`. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#readoutofbounds) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#readoutofbounds) + */ ReadOutOfBounds = 1009, - /** The project configuration cannot be decoded. + /** + * The project configuration cannot be decoded. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#cannotdecode) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#cannotdecode) + */ CannotDecode = 1010, // === Execution API errors === // https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ExecutionApi.scala - /** The provided execution stack item could not be found. + /** + * The provided execution stack item could not be found. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#stackitemnotfounderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#stackitemnotfounderror) + */ StackItemNotFound = 2001, - /** The provided exeuction context could not be found. + /** + * The provided exeuction context could not be found. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#contextnotfounderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#contextnotfounderror) + */ ContextNotFound = 2002, - /** The execution stack is empty. + /** + * The execution stack is empty. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#emptystackerror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#emptystackerror) + */ EmptyStack = 2003, - /** The stack is invalid in this context. + /** + * The stack is invalid in this context. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#invalidstackitemerror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#invalidstackitemerror) + */ InvalidStackItem = 2004, - /** The provided module could not be found. + /** + * The provided module could not be found. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#modulenotfounderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#modulenotfounderror) + */ ModuleNotFound = 2005, - /** The provided visualization could not be found. + /** + * The provided visualization could not be found. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#visualizationnotfounderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#visualizationnotfounderror) + */ VisualizationNotFound = 2006, - /** The expression specified in the {@link VisualizationConfiguration} cannot be evaluated. + /** + * The expression specified in the {@link VisualizationConfiguration} cannot be evaluated. * * If relevant, a {@link Diagnostic} containing error details is returned as `payload`. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#visualizationexpressionerror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#visualizationexpressionerror) + */ VisualizationExpression = 2007, // === Text API errors === // https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/text/TextApi.scala - /** A file was not opened. + /** + * A file was not opened. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filenotopenederror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filenotopenederror) + */ FileNotOpened = 3001, - /** Validation has failed for a series of text edits. + /** + * Validation has failed for a series of text edits. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#texteditvalidationerror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#texteditvalidationerror) + */ TextEditValidation = 3002, - /** The version provided by a client does not match the version computed by the server. + /** + * The version provided by a client does not match the version computed by the server. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#invalidversionerror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#invalidversionerror) + */ InvalidVersion = 3003, - /** The client doesn't hold write lock to the buffer. + /** + * The client doesn't hold write lock to the buffer. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#writedeniederror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#writedeniederror) + */ WriteDenied = 3004, // === Capability API errors === // https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/capability/CapabilityApi.scala - /** The requested capability is not acquired. + /** + * The requested capability is not acquired. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#accessdeniederror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#accessdeniederror) + */ CapabilityNotAcquired = 5001, // === Session API errors === // https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/session/SessionApi.scala - /** The request could not be proccessed, beacuse the session is not initialised. + /** + * The request could not be proccessed, beacuse the session is not initialised. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#sessionnotinitialisederror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#sessionnotinitialisederror) + */ SessionNotInitialised = 6001, - /** The session is already initialised. + /** + * The session is already initialised. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#sessionalreadyinitialisederror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#sessionalreadyinitialisederror) + */ SessionAlreadyInitialised = 6002, // === Search API errors === // https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/search/SearchApi.scala - /** There was an unexpected error accessing the suggestions database. + /** + * There was an unexpected error accessing the suggestions database. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#suggestionsdatabaseerror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#suggestionsdatabaseerror) + */ SuggestionsDatabase = 7001, - /** The project was not found in the root directory. + /** + * The project was not found in the root directory. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#projectnotfounderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#projectnotfounderror) + */ ProjectNotFound = 7002, - /** The module name could not be resolved for the given file. + /** + * The module name could not be resolved for the given file. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#modulenamenotresolvederror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#modulenamenotresolvederror) + */ ModuleNameNotResolved = 7003, - /** The requested suggestion could not be found. + /** + * The requested suggestion could not be found. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#suggestionnotfounderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#suggestionnotfounderror) + */ SuggestionNotFound = 7004, // === Library API errors === // https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryApi.scala - /** The requested edition could not be found. + /** + * The requested edition could not be found. * * The requested edition is returned in `payload.editionName`. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#editionnotfounderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#editionnotfounderror) + */ EditionNotFound = 8001, - /** A local library with the specified namespace and name combination already exists, so it cannot be created again. + /** + * A local library with the specified namespace and name combination already exists, so it cannot be created again. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#libraryalreadyexists) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#libraryalreadyexists) + */ LibraryAlreadyExists = 8002, - /** Authentication to the library repository was declined. + /** + * Authentication to the library repository was declined. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#libraryrepositoryauthenticationerror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#libraryrepositoryauthenticationerror) + */ LibraryRepositoryAuthentication = 8003, - /** A request to the library repository failed. + /** + * A request to the library repository failed. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarypublisherror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarypublisherror) + */ LibraryPublish = 8004, - /** Uploading the library failed for network-related reasons. + /** + * Uploading the library failed for network-related reasons. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#libraryuploaderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#libraryuploaderror) + */ LibraryUpload = 8005, - /** Downloading the library failed for network-related reasons, or the library was not found in the repository. + /** + * Downloading the library failed for network-related reasons, or the library was not found in the repository. * * The requested library is returned in `payload.namespace`, `payload.name`, and `payload.version`. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarydownloaderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarydownloaderror) + */ LibraryDownload = 8006, - /** A local library with the specified namespace and name combination was not found on the local libraries path. + /** + * A local library with the specified namespace and name combination was not found on the local libraries path. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#locallibrarynotfound) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#locallibrarynotfound) + */ LocalLibraryNotFound = 8007, - /** A library could not be resolved. It was not defined in the edition, and the settings did not + /** + * A library could not be resolved. It was not defined in the edition, and the settings did not * allow to resolve local libraries, or it did not exist there either. * * The requested namespace and name are returned in `payload.namespace` and `payload.name`. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarynotresolved) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#librarynotresolved) + */ LibraryNotResolved = 8008, - /** The chosen library name is invalid. + /** + * The chosen library name is invalid. * * A similar, valid name is returned in `payload.suggestedName`. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#invalidlibraryname) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#invalidlibraryname) + */ InvalidLibraryName = 8009, - /** The library preinstall endpoint could not properly find dependencies of the requested library. + /** + * The library preinstall endpoint could not properly find dependencies of the requested library. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#dependencydiscoveryerror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#dependencydiscoveryerror) + */ DependencyDiscovery = 8010, - /** The provided version string is not a valid semver version. + /** + * The provided version string is not a valid semver version. * * The requested version is returned in `payload.version`. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#invalidsemverversion) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#invalidsemverversion) + */ InvalidSemverVersion = 8011, // === Refactoring API errors === // https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/refactoring/RefactoringApi.scala - /** An expression with the provided ID could not be found. + /** + * An expression with the provided ID could not be found. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#expressionnotfounderror) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#expressionnotfounderror) + */ ExpressionNotFound = 9001, - /** The refactoring operation was not able to apply the generated edits. + /** + * The refactoring operation was not able to apply the generated edits. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#failedtoapplyedits) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#failedtoapplyedits) + */ FailedToApplyEdits = 9002, - /** Refactoring of the given expression is not supported. + /** + * Refactoring of the given expression is not supported. * - * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#refactoringnotsupported) */ + * [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#refactoringnotsupported) + */ RefactoringNotSupported = 9003, } diff --git a/app/ydoc-shared/src/util/assert.ts b/app/ydoc-shared/src/util/assert.ts index 81882739fb2f..a88cc485d5ee 100644 --- a/app/ydoc-shared/src/util/assert.ts +++ b/app/ydoc-shared/src/util/assert.ts @@ -1,7 +1,17 @@ +/** + * Assert that the current branch should be unreachable. + * This function should never be called at runtime due to its parameter being of type `never`. + * Being a type with zero values, it is impossible to construct an instance of this type at + * runtime. + */ export function assertNever(x: never): never { bail('Unexpected object: ' + JSON.stringify(x)) } +/** + * A type assertion that a condition is `true`. + * Throw an error if the condtion is `false`. + */ export function assert(condition: boolean, message?: string): asserts condition { if (!condition) bail(message ? `Assertion failed: ${message}` : 'Assertion failed') } @@ -13,7 +23,6 @@ export function assert(condition: boolean, message?: string): asserts condition * @param iterable The iterable to check. * @param length The expected length of the iterable. * @param message Optional message for the assertion error. - * @return void * @throws Error Will throw an error if the length does not match. * * The first five elements of the iterable will be displayed in the error message @@ -33,25 +42,40 @@ export function assertLength(iterable: Iterable, length: number, message?: ) } +/** + * Assert that an iterable contains zero elements. + */ export function assertEmpty(iterable: Iterable, message?: string): void { assertLength(iterable, 0, message) } +/** + * Assert that two values are equal (by reference for reference types, by value for value types). + */ export function assertEqual(actual: T, expected: T, message?: string) { const messagePrefix = message ? message + ' ' : '' assert(actual === expected, `${messagePrefix}Expected ${expected}, got ${actual}.`) } +/** + * Assert that two values are not equal (by reference for reference types, by value for value types). + */ export function assertNotEqual(actual: T, unexpected: T, message?: string) { const messagePrefix = message ? message + ' ' : '' assert(actual !== unexpected, `${messagePrefix}Expected not ${unexpected}, got ${actual}.`) } +/** + * A type assertion that a given value is not `undefined`. + */ export function assertDefined(x: T | undefined, message?: string): asserts x is T { const messagePrefix = message ? message + ' ' : '' assert(x !== undefined, `${messagePrefix}Expected value to be defined.`) } +/** + * Assert that this case is unreachable. + */ export function assertUnreachable(): never { bail('Unreachable code') } diff --git a/app/ydoc-shared/src/util/data/__tests__/text.test.ts b/app/ydoc-shared/src/util/data/__tests__/text.test.ts index a8dfbdd3e87f..63c02dacd083 100644 --- a/app/ydoc-shared/src/util/data/__tests__/text.test.ts +++ b/app/ydoc-shared/src/util/data/__tests__/text.test.ts @@ -15,7 +15,8 @@ test.prop({ expect(applyTextEdits(beforeString, edits)).toBe(afterString) }) -/** Test that `textChangeToEdits` and `applyTextEdits` work when inputs contain any special characters representable by +/** + * Test that `textChangeToEdits` and `applyTextEdits` work when inputs contain any special characters representable by * a `string`, including newlines and even incomplete surrogate pairs (invalid Unicode). */ test.prop({ @@ -29,7 +30,8 @@ test.prop({ expect(applyTextEdits(beforeString, edits)).toBe(afterString) }) -/** Tests that: +/** + * Tests that: * - When the code in `a[0]` is edited to become the code in `b[0]`, * `applyTextEditsToSpans` followed by `trimEnd` transforms the spans in `a.slice(1)` into the spans in `b.slice(1)`. * - The same holds when editing from `b` to `a`. @@ -39,7 +41,8 @@ function checkCorrespondence(a: string[], b: string[]) { checkCorrespondenceForward(b, a) } -/** Performs the same check as {@link checkCorrespondence}, for correspondences that are not expected to be reversible. +/** + Performs the same check as {@link checkCorrespondence}, for correspondences that are not expected to be reversible. */ function checkCorrespondenceForward(before: string[], after: string[]) { const leadingSpacesAndLength = (input: string): [number, number] => [ diff --git a/app/ydoc-shared/src/util/data/iterable.ts b/app/ydoc-shared/src/util/data/iterable.ts index 1e338408acef..f50efc5179b9 100644 --- a/app/ydoc-shared/src/util/data/iterable.ts +++ b/app/ydoc-shared/src/util/data/iterable.ts @@ -1,7 +1,14 @@ /** @file Functions for manipulating {@link Iterable}s. */ +/** + * An iterable with zero elements. + */ export function* empty(): Generator {} +/** + * An iterable `yield`ing numeric values with the given step between the start (inclusive) + * and the end (exclusive). + */ export function* range(start: number, stop: number, step = start <= stop ? 1 : -1) { if ((step > 0 && start > stop) || (step < 0 && start < stop)) { throw new Error( @@ -21,22 +28,38 @@ 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) } } +/** + * 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 } +/** + * Return an {@link Iterable} that `yield`s values from each iterable sequentially, + * yielding values from the second iterable only after exhausting the first iterable, and so on. + */ export function* chain(...iters: Iterable[]) { for (const iter of iters) { yield* iter } } +/** + * Return an iterable that `yield`s the next value from both given source iterables at the same time. + * Stops `yield`ing when *either* iterable is exhausted. + */ export function* zip(left: Iterable, right: Iterable): Generator<[T, U]> { const leftIterator = left[Symbol.iterator]() const rightIterator = right[Symbol.iterator]() @@ -48,6 +71,10 @@ export function* zip(left: Iterable, right: Iterable): Generator<[T, } } +/** + * Return an iterable that `yield`s the next value from both given source iterables at the same time. + * `yield`s `undefined` for the shorter iterator when it is exhausted. + */ export function* zipLongest( left: Iterable, right: Iterable, @@ -65,6 +92,10 @@ export function* zipLongest( } } +/** + * Return the value of the iterator if and only if it contains exactly one value. + * Otherwise, return `undefined`. + */ export function tryGetSoleValue(iter: Iterable): T | undefined { const iterator = iter[Symbol.iterator]() const result = iterator.next() @@ -74,25 +105,35 @@ export function tryGetSoleValue(iter: Iterable): T | undefined { return result.value } -/** Utility to simplify consuming an iterator a part at a time. */ +/** Utility to simplify consuming an iterator one part at a time. */ export class Resumable { private readonly iterator: Iterator private current: IteratorResult + /** + * Create a {@link Resumable}. + */ constructor(iterable: Iterable) { this.iterator = iterable[Symbol.iterator]() this.current = this.iterator.next() } + /** + * The current value of the iterator. + */ peek() { return this.current.done ? undefined : this.current.value } + /** + * Advance the iterator, saving the new current value of the iterator. + */ advance() { this.current = this.iterator.next() } - /** The given function peeks at the current value. If the function returns `true`, the current value will be advanced - * and the function called again; if it returns `false`, the peeked value remains current and `advanceWhile` returns. + /** + * The given function peeks at the current value. If the function returns `true`, the current value will be advanced + * and the function called again; if it returns `false`, the peeked value remains current and `advanceWhile` returns. */ advanceWhile(f: (value: T) => boolean) { while (!this.current.done && f(this.current.value)) { diff --git a/app/ydoc-shared/src/util/data/opt.ts b/app/ydoc-shared/src/util/data/opt.ts index 968c03b25b15..db191f3ef30c 100644 --- a/app/ydoc-shared/src/util/data/opt.ts +++ b/app/ydoc-shared/src/util/data/opt.ts @@ -1,6 +1,7 @@ /** @file A value that may be `null` or `undefined`. */ -/** Optional value type. This is a replacement for `T | null | undefined` that is more +/** + * Optional value type. This is a replacement for `T | null | undefined` that is more * convenient to use. We do not select a single value to represent "no value", because we are using * libraries that disagree whether `null` (e.g. Yjs) or `undefined` (e.g. Vue) should be used for * that purpose. We want to be compatible with both without needless conversions. In our own code, @@ -9,17 +10,28 @@ * `isSome` function. * * Note: For JSON-serialized data, prefer explicit `null` over `undefined`, since `undefined` is - * not serializable. Alternatively, use optional field syntax (e.g. `{ x?: number }`). */ + * not serializable. Alternatively, use optional field syntax (e.g. `{ x?: number }`). + */ export type Opt = T | null | undefined +/** + * Whether the given {@link Opt} is non-nullish. + */ export function isSome(value: Opt): value is T { return value != null } +/** + * Whether the given {@link Opt} is nullish. + */ export function isNone(value: Opt): value is null | undefined { return value == null } +/** + * Map the value inside the given {@link Opt} if it is not nullish, + * else return the given fallback value. + */ export function mapOr(optional: Opt, fallback: R, mapper: (value: T) => R): R { return isSome(optional) ? mapper(optional) : fallback } diff --git a/app/ydoc-shared/src/util/data/result.ts b/app/ydoc-shared/src/util/data/result.ts index 384bddb65f06..1b2e2f9946fc 100644 --- a/app/ydoc-shared/src/util/data/result.ts +++ b/app/ydoc-shared/src/util/data/result.ts @@ -1,5 +1,7 @@ -/** @file A generic type that can either hold a value representing a successful result, - * or an error. */ +/** + * @file A generic type that can either hold a value representing a successful result, + * or an error. + */ import { isSome, type Opt } from './opt' @@ -31,6 +33,9 @@ export type Result = */ export function Ok(): Result export function Ok(data: T): Result +/** + * Implementation of `Ok` constructor. + */ export function Ok(data?: T): Result { return { ok: true, value: data } } @@ -97,6 +102,9 @@ export function transposeResult(value: Opt>): Result, * If any of the values is an error, the first error is returned. */ export function transposeResult(value: Result[]): Result +/** + * Implementation of `transposeResult`. + */ export function transposeResult(value: Opt> | Result[]) { if (value == null) return Ok(value) if (value instanceof Array) { @@ -128,6 +136,9 @@ export class ResultError { /** All contexts attached by {@link withContext} function */ context: (() => string)[] + /** + * Create an {@link ResultError}. + */ constructor(payload: E) { this.payload = payload this.context = [] @@ -193,6 +204,9 @@ export function withContext( context: () => string, f: () => Promise>, ): Promise> +/** + * Implementation of `withContext`. + */ export function withContext( context: () => string, f: () => Promise> | Result, diff --git a/app/ydoc-shared/src/util/data/text.ts b/app/ydoc-shared/src/util/data/text.ts index fdd680791c7d..5615b02c74da 100644 --- a/app/ydoc-shared/src/util/data/text.ts +++ b/app/ydoc-shared/src/util/data/text.ts @@ -60,11 +60,13 @@ export function offsetEdit(textEdit: SourceRangeEdit, offset: number): SourceRan return { ...textEdit, range: [textEdit.range[0] + offset, textEdit.range[1] + offset] } } -/** Given: +/** + * Given: * @param textEdits - A change described by a set of text edits. * @param spansBefore - A collection of spans in the text before the edit. * @returns - A sequence of: Each span from `spansBefore` paired with the smallest span of the text after the edit that - * contains all text that was in the original span and has not been deleted. */ + * contains all text that was in the original span and has not been deleted. + */ export function applyTextEditsToSpans(textEdits: SourceRangeEdit[], spansBefore: SourceRange[]) { // Gather start and end points. const numerically = (a: number, b: number) => a - b @@ -117,7 +119,8 @@ export interface SpanTree { children(): IterableIterator> } -/** Given a span tree and some ranges, for each range find the smallest node that fully encloses it. +/** + * Given a span tree and some ranges, for each range find the smallest node that fully encloses it. * Return nodes paired with the ranges that are most closely enclosed by them. */ export function enclosingSpans( diff --git a/app/ydoc-shared/src/util/net.ts b/app/ydoc-shared/src/util/net.ts index 63292309d21b..78b957d7beae 100644 --- a/app/ydoc-shared/src/util/net.ts +++ b/app/ydoc-shared/src/util/net.ts @@ -17,16 +17,28 @@ interface Disposable { dispose(): void } +/** + * A scope which controls + */ export class AbortScope { private ctrl: AbortController = new AbortController() + /** + * Get the {@link AbortSignal} for this {@link AbortScope}. + */ get signal() { return this.ctrl.signal } + /** + * Trigger an abort for all listeners of this {@link AbortScope}. + */ dispose(reason?: string) { this.ctrl.abort(reason) } + /** + * Trigger disposal of the given {@link Disposable} when this {@link AbortScope} is aborted. + */ handleDispose(disposable: Disposable) { this.signal.throwIfAborted() this.onAbort(disposable.dispose.bind(disposable)) @@ -42,6 +54,9 @@ export class AbortScope { return child } + /** + * Call the given callback when this {@link AbortScope} is aborted. + */ onAbort(listener: () => void) { if (this.signal.aborted) { queueMicrotask(listener) @@ -50,6 +65,10 @@ export class AbortScope { } } + /** + * Add the given event listener on the given event on the given observable, + * removing the event listener when this {@link AbortScope} is aborted. + */ handleObserve< EVENTS extends { [key in keyof EVENTS]: (...arg0: any[]) => void }, NAME extends keyof EVENTS & string, @@ -66,10 +85,12 @@ export interface BackoffOptions { retryDelay?: number retryDelayMultiplier?: number retryDelayMax?: number - /** Called when the promise throws an error, and the next retry is about to be attempted. + /** + * Called when the promise throws an error, and the next retry is about to be attempted. * When this function returns `false`, the backoff is immediately aborted. When this function * is not provided, the backoff will always continue until the maximum number of retries - * is reached. * */ + * is reached. * + */ onBeforeRetry?: ( error: ResultError, retryCount: number, @@ -78,9 +99,11 @@ export interface BackoffOptions { ) => boolean | void /** Called right before returning. */ onSuccess?: (retryCount: number) => void - /** Called after the final retry, right before throwing an error. + /** + * Called after the final retry, right before throwing an error. * Note that `onBeforeRetry` is *not* called on the final retry, as there is nothing after the - * final retry. */ + * final retry. + */ onFailure?: (error: ResultError, retryCount: number) => void } @@ -132,6 +155,9 @@ export async function exponentialBackoff( } } +/** + * An `onBeforeRetry` handler used in {@link printingCallbacks} that logs an error. + */ export function defaultOnBeforeRetry( description: string, ): NonNullable['onBeforeRetry']> { @@ -145,6 +171,9 @@ export function defaultOnBeforeRetry( } } +/** + * An `onFailure` handler used in {@link printingCallbacks} that logs an error. + */ export function defaultOnFailure( description: string, ): NonNullable['onFailure']> { @@ -156,6 +185,9 @@ export function defaultOnFailure( } } +/** + * An `onSuccess` handler used in {@link printingCallbacks} that logs a message. + */ export function defaultOnSuccess( description: string, ): NonNullable['onSuccess']> { @@ -169,8 +201,10 @@ export function defaultOnSuccess( } } -/** @param successDescription Should be in past tense, without an initial capital letter. - * @param errorDescription Should be in present tense, without an initial capital letter. */ +/** + * @param successDescription Should be in past tense, without an initial capital letter. + * @param errorDescription Should be in present tense, without an initial capital letter. + */ export function printingCallbacks(successDescription: string, errorDescription: string) { return { onBeforeRetry: defaultOnBeforeRetry(errorDescription), diff --git a/app/ydoc-shared/src/util/net/MockWSTransport.ts b/app/ydoc-shared/src/util/net/MockWSTransport.ts index 3b94057c3bc3..04487547ac1b 100644 --- a/app/ydoc-shared/src/util/net/MockWSTransport.ts +++ b/app/ydoc-shared/src/util/net/MockWSTransport.ts @@ -12,22 +12,47 @@ export interface MockTransportData { (method: Methods, params: any, transport: MockWebSocketTransport): Promise } +/** + * A mock WebSocket transport, only for use in tests. + */ export class MockWebSocketTransport extends ReconnectingWebSocketTransport { static mocks: Map = new Map() private openEventListeners = new Set<(event: WebSocketEventMap['open']) => void>() + /** + * Create an {@link MockWebSocketTransport}. + */ constructor(public name: string) { super('') } + /** + * Add a handler for the {@link MockWebSocketTransport} with the given name. + */ static addMock(name: string, data: MockTransportData) { MockWebSocketTransport.mocks.set(name, data as any) } + /** + * Simulate connecting to a WebSocket. + */ override connect(): Promise { for (const listener of this.openEventListeners) listener(new Event('open')) return Promise.resolve() } + /** + * Simulate reconnecting to a WebSocket. + * Currently unimplemented as the functionality is not needed for tests. + */ override reconnect() {} + /** + * Simulate closing a WebSocket. + * Currently unimplemented as the functionality is not needed for tests. + */ override close(): void {} + /** + * Respond to the given JSON-RPC request, calling the mock implementation + * registered with {@link MockWebSocketTransport['addMock']}. + * Returns a rejected {@link Promise} if there is no corresponding mock implementation. + */ override sendData(data: JSONRPCRequestData, timeout?: number | null): Promise { if (Array.isArray(data)) return Promise.all(data.map(d => this.sendData(d.request, timeout))) return ( @@ -38,6 +63,9 @@ export class MockWebSocketTransport extends ReconnectingWebSocketTransport { ) ?? Promise.reject() ) } + /** + * Emit a JSON-RPC notification. + */ emit(method: N, params: ArgumentsType[0]): void { this.transportRequestManager.transportEventChannel.emit('notification', { jsonrpc: '2.0', @@ -46,22 +74,26 @@ export class MockWebSocketTransport extends ReconnectingWebSocketTransport { } as IJSONRPCNotificationResponse) } + /** + * Add an event listener for the given event. + */ override on( type: K, cb: ( event: WebSocketEventMap[K] extends Event ? WebSocketEventMap[K] : never, ) => WebSocketEventMap[K] extends Event ? void : never, - options?: AddEventListenerOptions, ): void { if (type === 'open') this.openEventListeners.add(cb as any) } + /** + * Remove an event listener for the given event. + */ override off( type: K, cb: ( event: WebSocketEventMap[K] extends Event ? WebSocketEventMap[K] : never, ) => WebSocketEventMap[K] extends Event ? void : never, - options?: AddEventListenerOptions, ): void { if (type === 'open') this.openEventListeners.delete(cb as any) } diff --git a/app/ydoc-shared/src/util/net/ReconnectingWSTransport.ts b/app/ydoc-shared/src/util/net/ReconnectingWSTransport.ts index 753530a31e21..5b55262f877f 100644 --- a/app/ydoc-shared/src/util/net/ReconnectingWSTransport.ts +++ b/app/ydoc-shared/src/util/net/ReconnectingWSTransport.ts @@ -18,8 +18,14 @@ export interface AddEventListenerOptions { signal?: AbortSignal } +/** + * A socket that automatically connects upon disconnect, for example after network issues. + */ export class ReconnectingWebSocketTransport extends WebSocketTransport { private _reconnectingConnection: ReconnectingWebSocket + /** + * Create a {@link ReconnectingWebSocketTransport}. + */ constructor(uri: string, wsOptions: Options = {}) { super(uri) this.uri = uri @@ -31,10 +37,16 @@ export class ReconnectingWebSocketTransport extends WebSocketTransport { this.connection = this._reconnectingConnection as any } + /** + * Reconnect the underlying WebSocket. + */ public reconnect() { this._reconnectingConnection.reconnect() } + /** + * Add an event listener to the underlying WebSocket. + */ on( type: K, cb: ( @@ -45,6 +57,9 @@ export class ReconnectingWebSocketTransport extends WebSocketTransport { this._reconnectingConnection.addEventListener(type, cb, options) } + /** + * Remove an event listener from the underlying WebSocket. + */ off( type: K, cb: ( diff --git a/app/ydoc-shared/src/util/types.ts b/app/ydoc-shared/src/util/types.ts index 8a443e38d592..4f62800d8df1 100644 --- a/app/ydoc-shared/src/util/types.ts +++ b/app/ydoc-shared/src/util/types.ts @@ -1,5 +1,7 @@ -/** Returns an all the keys of a type. The argument provided is required to be an object containing all the keys of the - * type (including optional fields), but the associated values are ignored and may be of any type. */ +/** + * Returns an all the keys of a type. The argument provided is required to be an object containing all the keys of the + * type (including optional fields), but the associated values are ignored and may be of any type. + */ export function allKeys(keys: { [P in keyof T]-?: any }): ReadonlySet { return Object.freeze(new Set(Object.keys(keys))) } diff --git a/app/ydoc-shared/src/uuid.ts b/app/ydoc-shared/src/uuid.ts index 998ddc5ce92d..6402abce4bfa 100644 --- a/app/ydoc-shared/src/uuid.ts +++ b/app/ydoc-shared/src/uuid.ts @@ -1,11 +1,17 @@ import type { Uuid } from './yjsModel' +/** + * Return the textual representation of a UUID from its 64 high and 64 low bits. + */ export function uuidFromBits(leastSigBits: bigint, mostSigBits: bigint): Uuid { const bits = (mostSigBits << 64n) | leastSigBits const string = bits.toString(16).padStart(32, '0') return string.replace(/(........)(....)(....)(....)(............)/, '$1-$2-$3-$4-$5') as Uuid } +/** + * Return the 64 high and 64 low bits of a UUID from its textual representation. + */ export function uuidToBits(uuid: string): [leastSigBits: bigint, mostSigBits: bigint] { const bits = BigInt('0x' + uuid.replace(/-/g, '')) return [bits & 0xffffffffffffffffn, bits >> 64n] diff --git a/app/ydoc-shared/src/yjsModel.ts b/app/ydoc-shared/src/yjsModel.ts index aae5e4efb527..4a55c0b97d29 100644 --- a/app/ydoc-shared/src/yjsModel.ts +++ b/app/ydoc-shared/src/yjsModel.ts @@ -25,6 +25,9 @@ export interface VisualizationMetadata { height: number | null } +/** + * + */ export function visMetadataEquals( a: VisualizationMetadata | null | undefined, b: VisualizationMetadata | null | undefined, @@ -40,6 +43,9 @@ export function visMetadataEquals( ) } +/** + * + */ export function visIdentifierEquals( a: VisualizationIdentifier | null | undefined, b: VisualizationIdentifier | null | undefined, @@ -49,12 +55,18 @@ export function visIdentifierEquals( export type ProjectSetting = string +/** + * + */ export class DistributedProject { doc: Y.Doc name: Y.Text modules: Y.Map settings: Y.Map + /** + * + */ constructor(doc: Y.Doc) { this.doc = doc this.name = this.doc.getText('name') @@ -62,10 +74,16 @@ export class DistributedProject { this.settings = this.doc.getMap('settings') } + /** + * + */ moduleNames(): string[] { return Array.from(this.modules.keys()) } + /** + * + */ findModuleByDocId(id: string): string | null { for (const [name, doc] of this.modules.entries()) { if (doc.guid === id) return name @@ -73,60 +91,96 @@ export class DistributedProject { return null } + /** + * + */ async openModule(name: string): Promise { const doc = this.modules.get(name) if (doc == null) return null return await DistributedModule.load(doc) } + /** + * + */ openUnloadedModule(name: string): DistributedModule | null { const doc = this.modules.get(name) if (doc == null) return null return new DistributedModule(doc) } + /** + * + */ createUnloadedModule(name: string, doc: Y.Doc): DistributedModule { this.modules.set(name, doc) return new DistributedModule(doc) } + /** + * + */ createNewModule(name: string): DistributedModule { return this.createUnloadedModule(name, new Y.Doc()) } + /** + * + */ deleteModule(name: string): void { this.modules.delete(name) } + /** + * + */ dispose(): void { this.doc.destroy() } } +/** + * + */ export class ModuleDoc { ydoc: Y.Doc nodes: Y.Map + /** + * + */ constructor(ydoc: Y.Doc) { this.ydoc = ydoc this.nodes = ydoc.getMap('nodes') } } +/** + * + */ export class DistributedModule { doc: ModuleDoc undoManager: Y.UndoManager + /** + * + */ static async load(ydoc: Y.Doc): Promise { ydoc.load() await ydoc.whenLoaded return new DistributedModule(ydoc) } + /** + * + */ constructor(ydoc: Y.Doc) { this.doc = new ModuleDoc(ydoc) this.undoManager = new Y.UndoManager([this.doc.nodes]) } + /** + * + */ dispose(): void { this.doc.ydoc.destroy() } @@ -137,10 +191,16 @@ export type LocalUserActionOrigin = (typeof localUserActionOrigins)[number] export type Origin = LocalUserActionOrigin | 'remote' | 'local:autoLayout' /** Locally-originated changes not otherwise specified. */ export const defaultLocalOrigin: LocalUserActionOrigin = 'local:userAction' +/** + * + */ export function isLocalUserActionOrigin(origin: string): origin is LocalUserActionOrigin { const localOriginNames: readonly string[] = localUserActionOrigins return localOriginNames.includes(origin) } +/** + * + */ export function tryAsOrigin(origin: string): Origin | undefined { if (isLocalUserActionOrigin(origin)) return origin if (origin === 'local:autoLayout') return origin @@ -151,34 +211,58 @@ export type SourceRange = readonly [start: number, end: number] declare const brandSourceRangeKey: unique symbol export type SourceRangeKey = string & { [brandSourceRangeKey]: never } +/** + * + */ export function sourceRangeKey(range: SourceRange): SourceRangeKey { return `${range[0].toString(16)}:${range[1].toString(16)}` as SourceRangeKey } +/** + * + */ export function sourceRangeFromKey(key: SourceRangeKey): SourceRange { return key.split(':').map(x => parseInt(x, 16)) as [number, number] } +/** + * + */ export class IdMap { private readonly rangeToExpr: Map + /** + * + */ constructor(entries?: [string, ExternalId][]) { this.rangeToExpr = new Map(entries ?? []) } + /** + * + */ static Mock(): IdMap { return new IdMap([]) } + /** + * + */ insertKnownId(range: SourceRange, id: ExternalId) { const key = sourceRangeKey(range) this.rangeToExpr.set(key, id) } + /** + * + */ getIfExist(range: SourceRange): ExternalId | undefined { const key = sourceRangeKey(range) return this.rangeToExpr.get(key) } + /** + * + */ getOrInsertUniqueId(range: SourceRange): ExternalId { const key = sourceRangeKey(range) const val = this.rangeToExpr.get(key) @@ -191,18 +275,30 @@ export class IdMap { } } + /** + * + */ entries(): [SourceRangeKey, ExternalId][] { return [...this.rangeToExpr] as [SourceRangeKey, ExternalId][] } + /** + * + */ get size(): number { return this.rangeToExpr.size } + /** + * + */ clear(): void { this.rangeToExpr.clear() } + /** + * + */ isEqual(other: IdMap): boolean { if (other.size !== this.size) return false for (const [key, value] of this.rangeToExpr.entries()) { @@ -212,6 +308,9 @@ export class IdMap { return true } + /** + * + */ validate() { const uniqueValues = new Set(this.rangeToExpr.values()) if (uniqueValues.size < this.rangeToExpr.size) { @@ -219,11 +318,17 @@ export class IdMap { } } + /** + * + */ clone(): IdMap { return new IdMap(this.entries()) } // Debugging. + /** + * + */ compare(other: IdMap) { console.info(`IdMap.compare -------`) const allKeys = new Set() @@ -240,26 +345,44 @@ export class IdMap { } const uuidRegex = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/ +/** + * + */ export function isUuid(x: unknown): x is Uuid { return typeof x === 'string' && x.length === 36 && uuidRegex.test(x) } +/** + * + */ export function rangeEquals(a: SourceRange, b: SourceRange): boolean { return a[0] == b[0] && a[1] == b[1] } +/** + * + */ export function rangeIncludes(a: SourceRange, b: number): boolean { return a[0] <= b && a[1] >= b } +/** + * + */ export function rangeLength(a: SourceRange): number { return a[1] - a[0] } +/** + * + */ export function rangeEncloses(a: SourceRange, b: SourceRange): boolean { return a[0] <= b[0] && a[1] >= b[1] } +/** + * + */ export function rangeIntersects(a: SourceRange, b: SourceRange): boolean { return a[0] <= b[1] && a[1] >= b[0] } diff --git a/eslint.config.mjs b/eslint.config.mjs index d51443827fa5..5da5d1ec3153 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,8 @@ /** @file ESLint configuration file. */ -/** NOTE: The "Experimental: Use Flat Config" option must be enabled. - * Flat config is still not quite mature, so is disabled by default. */ +/** + * NOTE: The "Experimental: Use Flat Config" option must be enabled. + * Flat config is still not quite mature, so is disabled by default. + */ import * as path from 'node:path' import * as url from 'node:url' @@ -24,13 +26,15 @@ import globals from 'globals' const DEBUG_STATEMENTS_MESSAGE = 'Avoid leaving debugging statements when committing code' const DIR_NAME = path.dirname(url.fileURLToPath(import.meta.url)) const NAME = 'enso' -/** An explicit whitelist of CommonJS modules, which do not support namespace imports. +/** + * An explicit whitelist of CommonJS modules, which do not support namespace imports. * Many of these have incorrect types, so no type error may not mean they support ESM, * and conversely type errors may not mean they don't support ESM - * but we add those to the whitelist anyway otherwise we get type errors. * In particular, `string-length` supports ESM but its type definitions don't. * `yargs` is a modules we explicitly want the default imports of. - * `node:process` is here because `process.on` does not exist on the namespace import. */ + * `node:process` is here because `process.on` does not exist on the namespace import. + */ const DEFAULT_IMPORT_ONLY_MODULES = '@vitejs\\u002Fplugin-react|node:process|chalk|string-length|yargs|yargs\\u002Fyargs|sharp|to-ico|connect|morgan|serve-static|tiny-invariant|clsx|create-servers|electron-is-dev|fast-glob|esbuild-plugin-.+|opener|tailwindcss.*|@modyfi\\u002Fvite-plugin-yaml|build-info|is-network-error|validator.+|.*[.]json$' const RELATIVE_MODULES = @@ -48,7 +52,6 @@ const NOT_CONSTANT_CASE = `/^(?!${WHITELISTED_CONSTANTS}$|_?[A-Z][A-Z0-9]*(_[A-Z // Extracted to a variable because it needs to be used twice: // - once as-is for `.d.ts` // - once explicitly disallowing `declare`s in regular `.ts`. -/** @type {{ selector: string; message: string; }[]} */ const RESTRICTED_SYNTAXES = [ { selector: `ImportDeclaration[source.value=/^(?!(${ALLOWED_DEFAULT_IMPORT_MODULES})$)[^.]/] > ImportDefaultSpecifier`, @@ -195,6 +198,8 @@ export default [ '**/build.mjs', '**/*.timestamp-*.mjs', '**/node_modules', + '**/generated', + 'app/rust-ffi/pkg/', ], }, eslintJs.configs.recommended, @@ -207,7 +212,12 @@ export default [ tsconfigRootDir: DIR_NAME, ecmaVersion: 'latest', extraFileExtensions: ['.vue'], - projectService: true, + projectService: { + allowDefaultProject: [ + 'app/ydoc-server/vitest.config.ts', + 'app/ydoc-shared/vitest.config.ts', + ], + }, }, }, rules: {