Create execution context on GUI2 start (#7894)

Because several tasks require execution context, this is a fast PR making one.

# Important Notes
* Changes in languageServer.ts and languageServerTypes.ts were directly ported from #7873
* We display warning about missing namespace, because the dashboard does not provide us any. Needs to be fixed at some point.
This commit is contained in:
Adam Obuchowicz 2023-09-27 17:33:41 +02:00 committed by GitHub
parent b03712390c
commit cf16d32894
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 714 additions and 164 deletions

View File

@ -27,6 +27,7 @@
"@vueuse/core": "^10.4.1",
"codemirror": "^6.0.1",
"enso-authentication": "^1.0.0",
"events": "^3.3.0",
"install": "^0.13.0",
"hash-sum": "^2.0.0",
"isomorphic-ws": "^5.0.0",

View File

@ -1,61 +0,0 @@
import * as array from 'lib0/array'
import * as map from 'lib0/map'
import * as set from 'lib0/set'
type EventMap = { [name: PropertyKey]: any[] }
export type EventHandler<Args extends any[] = any[]> = (...args: Args) => void
export class Emitter<Events extends EventMap = EventMap> {
private _observers: Map<PropertyKey, Set<EventHandler<any[]>>>
constructor() {
this._observers = map.create()
}
on<N extends keyof Events>(name: N, f: EventHandler<Events[N]>) {
map.setIfUndefined(this._observers, name as PropertyKey, set.create).add(f)
}
once<N extends keyof Events>(name: N, f: EventHandler<Events[N]>) {
const _f = (...args: Events[N]) => {
this.off(name, _f)
f(...args)
}
this.on(name, _f)
}
off<N extends keyof Events>(name: N, f: EventHandler<Events[N]>) {
const observers = this._observers.get(name)
if (observers !== undefined) {
observers.delete(f as EventHandler<any[]>)
if (observers.size === 0) {
this._observers.delete(name)
}
}
}
emit<N extends keyof Events>(name: N, args: Events[N]) {
// copy all listeners to an array first to make sure that no event is emitted to listeners that
// are subscribed while the event handler is called.
return array
.from((this._observers.get(name) || map.create()).values())
.forEach((f) => f(...args))
}
destroy() {
this._observers = map.create()
}
}
// Partial compatibility with node 'events' module, for the purposes of @open-rpc/client-js.
// See vite.config.ts for details.
export class EventEmitter extends Emitter<EventMap> {
addListener(name: string, f: EventHandler) {
this.on(name, f)
}
removeListener(name: string, f: EventHandler) {
this.off(name, f)
}
removeAllListeners() {
this.destroy()
}
}

View File

@ -1,20 +1,28 @@
import { Client } from '@open-rpc/client-js'
import * as map from 'lib0/map'
import { ObservableV2 } from 'lib0/observable'
import * as set from 'lib0/set'
import { SHA3 } from 'sha3'
import { Emitter } from './event'
import type {
Checksum,
ContextId,
ExecutionEnvironment,
ExpressionId,
FileEdit,
FileSystemObject,
Notifications,
Path,
RegisterOptions,
StackItem,
TextFileContents,
VisualizationConfiguration,
response,
} from './languageServerTypes'
import type { Uuid } from './yjsModel'
export class LanguageServer extends Emitter<Notifications> {
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md) */
export class LanguageServer extends ObservableV2<Notifications> {
client: Client
handlers: Map<string, Set<(...params: any[]) => void>>
@ -46,49 +54,236 @@ export class LanguageServer extends Emitter<Notifications> {
}
}
private request(method: string, params: object): Promise<any> {
// The "magic bag of holding" generic that is only present in the return type is UNSOUND.
// However, it is SAFE, as the return type of the API is statically known.
private request<T>(method: string, params: object): Promise<T> {
return this.client.request({ method, params })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#capabilityacquire) */
acquireCapability(method: string, registerOptions: RegisterOptions): Promise<void> {
return this.request('capability/acquire', { method, registerOptions })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filereceivestreeupdates) */
acquireReceivesTreeUpdates(path: Path): Promise<void> {
return this.acquireCapability('file/receivesTreeUpdates', { path })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#sessioninitprotocolconnection) */
initProtocolConnection(clientId: Uuid): Promise<response.InitProtocolConnection> {
return this.request('session/initProtocolConnection', { clientId })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#textopenfile) */
openTextFile(path: Path): Promise<response.OpenTextFile> {
return this.request('text/openFile', { path })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#textclosefile) */
closeTextFile(path: Path): Promise<void> {
return this.request('text/closeFile', { path })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#textsave) */
saveTextFile(path: Path, currentVersion: Checksum): Promise<void> {
return this.request('text/save', { path, currentVersion })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#textapplyedit) */
applyEdit(edit: FileEdit, execute: boolean): Promise<void> {
return this.request('text/applyEdit', { edit, execute })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filewrite) */
writeFile(path: Path, contents: TextFileContents): Promise<void> {
return this.request('file/write', { path, contents })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#fileread) */
readFile(path: Path): Promise<response.FileContents> {
return this.request('file/read', { path })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filecreate) */
createFile(object: FileSystemObject): Promise<void> {
return this.request('file/create', { object })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filedelete) */
deleteFile(path: Path): Promise<void> {
return this.request('file/delete', { path })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filecopy) */
copyFile(from: Path, to: Path): Promise<void> {
return this.request('file/copy', { from, to })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filemove) */
moveFile(from: Path, to: Path): Promise<void> {
return this.request('file/move', { from, to })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#fileexists) */
fileExists(path: Path): Promise<response.FileExists> {
return this.request('file/exists', { path })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filetree) */
fileTree(path: Path, depth?: number): Promise<response.FileTree> {
return this.request('file/tree', { path, depth })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filelist) */
listFiles(path: Path): Promise<response.FileList> {
return this.request('file/list', { path })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#fileinfo) */
fileInfo(path: Path): Promise<response.FileInfo> {
return this.request('file/info', { path })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filechecksum) */
fileChecksum(path: Path): Promise<response.FileChecksum> {
return this.request('file/checksum', { path })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#vcsinit) */
vcsInit(root: Path): Promise<void> {
return this.request('vcs/init', { root })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#vcssave) */
vcsSave(root: Path, name?: string): Promise<response.VCSCommit> {
return this.request('vcs/save', { root, name })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#vcsstatus) */
vcsStatus(root: Path): Promise<response.VCSStatus> {
return this.request('vcs/status', { root })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#vcsrestore) */
vcsRestore(root: Path, commitId?: string): Promise<response.VCSChanges> {
return this.request('vcs/restore', { root, commitId })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#vcslist) */
vcsList(root: Path, limit?: number): Promise<response.VCSSaves> {
return this.request('vcs/list', { root, limit })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextcreate) */
createExecutionContext(contextId?: ContextId): Promise<response.ExecutionContext> {
return this.request('executionContext/create', { contextId })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextdestroy) */
destroyExecutionContext(contextId: ContextId): Promise<void> {
return this.request('executionContext/destroy', { contextId })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextfork) */
forkExecutionContext(contextId: ContextId): Promise<response.ExecutionContext> {
return this.request('executionContext/fork', { contextId })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextpush) */
pushExecutionContextItem(contextId: ContextId, stackItem: StackItem): Promise<void> {
return this.request('executionContext/push', { contextId, stackItem })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextpop) */
popExecutionContextItem(contextId: ContextId): Promise<void> {
return this.request('executionContext/pop', { contextId })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextrecompute) */
recomputeExecutionContext(
contextId: ContextId,
invalidatedExpressions?: 'all' | string[],
executionEnvironment?: ExecutionEnvironment,
): Promise<void> {
return this.request('executionContext/recompute', {
contextId,
invalidatedExpressions,
executionEnvironment,
})
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextinterrupt) */
interruptExecutionContext(contextId: ContextId): Promise<void> {
return this.request('executionContext/interrupt', { contextId })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextsetexecutionenvironment) */
setExecutionEnvironment(
contextId: ContextId,
executionEnvironment?: ExecutionEnvironment,
): Promise<void> {
return this.request('executionContext/setExecutionEnvironment', {
contextId,
executionEnvironment,
})
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextexecuteexpression) */
executeExpression(
visualizationId: Uuid,
expressionId: ExpressionId,
visualizationConfig: VisualizationConfiguration,
): Promise<void> {
return this.request('executionContext/interrupt', {
visualizationId,
expressionId,
visualizationConfig,
})
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextattachvisualization) */
attachVisualization(
visualizationId: Uuid,
expressionId: ExpressionId,
visualizationConfig: VisualizationConfiguration,
): Promise<void> {
return this.request('executionContext/attachVisualization', {
visualizationId,
expressionId,
visualizationConfig,
})
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextdetachvisualization) */
detachVisualization(
visualizationId: Uuid,
expressionId: ExpressionId,
executionContextId: ContextId,
): Promise<void> {
return this.request('executionContext/detachVisualization', {
visualizationId,
expressionId,
executionContextId,
})
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#executioncontextmodifyvisualization) */
modifyVisualization(
visualizationId: Uuid,
visualizationConfig: VisualizationConfiguration,
): Promise<void> {
return this.request('executionContext/modifyVisualization', {
visualizationId,
visualizationConfig,
})
}
dispose() {
this.client.close()
}
}
export function computeTextChecksum(text: string): Checksum {
const hash = new SHA3(224)
hash.update(text)
return hash.digest('hex') as Checksum
return new SHA3(224).update(text).digest('hex') as Checksum
}

View File

@ -1,10 +1,13 @@
import type { Uuid } from './yjsModel'
import type { ExprId, Uuid } from './yjsModel'
/** Version checksum of a text file - Sha3_224 */
declare const brandChecksum: unique symbol
export type Checksum = string & { [brandChecksum]: never }
export type ContextId = Uuid
export type ExpressionId = Uuid
declare const brandContextId: unique symbol
export type ContextId = Uuid & { [brandContextId]: never }
export type ExpressionId = ExprId
declare const brandUtcDateTime: unique symbol
export type UTCDateTime = string & { [brandUtcDateTime]: never }
export type ContentRoot =
| { type: 'Project'; id: Uuid }
@ -28,6 +31,27 @@ export interface FileEdit {
newVersion: Checksum
}
export interface FileContents<T> {
contents: T
}
export interface TextFileContents extends FileContents<string> {}
export interface DirectoryTree {
path: Path
name: string
files: FileSystemObject[]
directories: DirectoryTree[]
}
export interface FileAttributes {
creationTime: UTCDateTime
lastAccessTime: UTCDateTime
lastModifiedTime: UTCDateTime
kind: FileSystemObject
byteSize: number
}
export interface TextEdit {
range: TextRange
text: string
@ -107,31 +131,21 @@ export interface Panic {
* provides description and percentage (`0.0-1.0`) of completeness.
*/
export interface Pending {
/**
* Optional message describing current operation.
*/
/** Optional message describing current operation. */
message?: string
/**
* Optional amount of already done work as a number between `0.0` to `1.0`.
*/
progress?: Number
/** Optional amount of already done work as a number between `0.0` to `1.0`. */
progress?: number
}
/**
* Information about warnings associated with the value.
*/
export interface Warnings {
/**
* The number of attached 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
}
@ -148,25 +162,25 @@ export interface FunctionSchema {
export interface MethodPointer {
/** The fully qualified module name. */
module: String
module: string
/** The type on which the method is defined. */
definedOnType: String
definedOnType: string
/** The method name. */
name: String
name: string
}
export type ProfilingInfo = ExecutionTime
interface ExecutionTime {
export interface ExecutionTime {
/** The time elapsed during the expression's evaluation, in nanoseconds */
nanoTime: Number
nanoTime: number
}
interface ExpressionUpdate {
export interface ExpressionUpdate {
/** The id of updated expression. */
expressionId: ExpressionId
/** The updated type of the expression. */
type?: String
type?: string
/** The updated method call info. */
methodCall?: MethodCall
/** Profiling information about the expression. */
@ -177,57 +191,38 @@ interface ExpressionUpdate {
payload: ExpressionUpdatePayload
}
interface StackTraceElement {
export interface StackTraceElement {
functionName: string
path?: Path
location?: TextRange
}
type DiagnosticType = 'Error' | 'Warning'
export type DiagnosticType = 'Error' | 'Warning'
interface Diagnostic {
/**
* The type of diagnostic message.
*/
export interface Diagnostic {
/** The type of diagnostic message. */
kind: DiagnosticType
/**
* The diagnostic message.
*/
message: String
/**
* The location of a file containing the diagnostic.
*/
/** The diagnostic message. */
message: string
/** The location of a file containing the diagnostic. */
path?: Path
/**
* The location of the diagnostic object in a file.
*/
location?: Range
/**
* The id of related expression.
*/
/** The location of the diagnostic object in a file. */
location?: TextRange
/** The id of related expression. */
expressionId?: ExpressionId
/**
* The stack trace.
*/
/** The stack trace. */
stack: StackTraceElement[]
}
/** A representation of what kind of type a filesystem object can be. */
type FileSystemObject =
export type FileSystemObject =
| {
type: 'Directory'
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
@ -255,28 +250,65 @@ type FileSystemObject =
interface VisualizationContext {}
export interface VisualizationConfiguration {
/** An execution context of the visualization. */
executionContextId: Uuid
/** 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
/** A list of arguments to pass to the visualization expression. */
positionalArgumentsExpressions?: string[]
}
export interface VCSSave {
commitId: string
message: string
}
export type Notifications = {
'file/event': [{ path: Path; kind: FileEventKind }]
'text/autoSave': [{ path: Path }]
'text/didChange': [{ edits: FileEdit[] }]
'text/fileModifiedOnDisk': [{ path: Path }]
'executionContext/expressionUpdates': [{ contextId: ContextId; updates: ExpressionUpdate[] }]
'executionContext/executionFailed': [{ contextId: ContextId; message: string }]
'executionContext/executionComplete': [{ contextId: ContextId }]
'executionContext/executionStatus': [{ contextId: ContextId; diagnostics: Diagnostic[] }]
'search/suggestionsDatabaseUpdate': [{}]
'file/rootAdded': [{}]
'file/rootRemoved': [{}]
'executionContext/visualizationEvaluationFailed': [
{
contextId: ContextId
visualizationId: Uuid
expressionId: ExpressionId
message: String
diagnostic?: Diagnostic
},
]
'refactoring/projectRenamed': [{}]
'text/autoSave': (param: { path: Path }) => void
'text/didChange': (param: { edits: FileEdit[] }) => void
'text/fileModifiedOnDisk': (param: { path: Path }) => void
'executionContext/expressionUpdates': (param: {
contextId: ContextId
updates: ExpressionUpdate[]
}) => void
'executionContext/executionFailed': (param: { contextId: ContextId; message: string }) => void
'executionContext/executionComplete': (param: { contextId: ContextId }) => void
'executionContext/executionStatus': (param: {
contextId: ContextId
diagnostics: Diagnostic[]
}) => void
'executionContext/visualizationEvaluationFailed': (param: {
contextId: ContextId
visualizationId: Uuid
expressionId: ExpressionId
message: string
diagnostic?: Diagnostic
}) => void
'search/suggestionsDatabaseUpdate': (param: {}) => void
'file/event': (param: { path: Path; kind: FileEventKind }) => void
'file/rootAdded': (param: {}) => void
'file/rootRemoved': (param: {}) => void
'refactoring/projectRenamed': (param: {}) => void
}
export type ExecutionEnvironment = 'Design' | 'Live'
export type StackItem = ExplicitCall | LocalCall
export interface ExplicitCall {
type: 'ExplicitCall'
methodPointer: MethodPointer
thisArgumentExpression?: string
positionalArgumentsExpressions: string[]
}
export interface LocalCall {
type: 'LocalCall'
expressionId: ExpressionId
}
export namespace response {
@ -290,12 +322,293 @@ export namespace response {
contentRoots: ContentRoot[]
}
export interface FileContents {
contents: TextFileContents
}
export interface FileExists {
exists: boolean
}
export interface FileTree {
tree: DirectoryTree
}
export interface FileList {
paths: FileSystemObject[]
}
export interface FileInfo {
attributes: FileAttributes
}
export interface FileChecksum {
checksum: Checksum
}
export interface VCSCommit {
commitId: string
message: string
}
export interface VCSStatus {
dirty: boolean
changed: Path[]
lastSave: VCSSave
}
export interface VCSChanges {
changed: Path[]
}
export interface VCSSaves {
saves: VCSSave[]
}
export interface ExecutionContext {
contextId: ContextId
canModify: CapabilityRegistration
receivesUpdates: CapabilityRegistration
}
export interface VisualizationUpdate {
context: VisualizationContext
data: Uint8Array
}
}
export interface LanguageServerError {
code: LanguageServerErrorCode
message: string
payload?: Record<string, string | number> | Diagnostic
}
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.
*
* [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#accessdeniederror) */
AccessDenied = 100,
// === VCS Manager API errors ===
// https://github.com/enso-org/enso/blob/develop/engine/language-server/src/main/scala/org/enso/languageserver/vcsmanager/VcsManagerApi.scala
// `ContentRootNotFound` is also defined by the File Manager API with the same code, so it is omitted here.
/** A miscellaneous VCS error. */
VCS = 1000,
/** The project was not found in the VCS. */
VCSProjectNotFound = 1002,
/** The project is not under version control. */
VCSNotFound = 1003,
/** The requested save could not be found. */
SaveNotFound = 1004,
/** The requested project is already under version control. */
VCSAlreadyExists = 1005,
// === 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.
*
* [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filesystemerror) */
FileSystem = 1000,
/** 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) */
ContentRootNotFound = 1001,
/** The requested file does not exist.
*
*[Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#filenotfound) */
FileNotFound = 1003,
/** 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) */
FileExists = 1004,
/** The IO operation timed out.
*
* [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#operationtimeouterror) */
OperationTimeoutError = 1005,
/** The provided path is not a directory.
*
* [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.
*
* [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.
*
* [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 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) */
ReadOutOfBounds = 1009,
/** The project configuration cannot be decoded.
*
* [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.
*
* [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.
*
* [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#contextnotfounderror) */
ContextNotFound = 2002,
/** The execution stack is empty.
*
* [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.
*
* [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.
*
* [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.
*
* [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.
*
* 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) */
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.
*
* [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.
*
* [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.
*
* [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.
*
* [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.
*
* [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.
*
* [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#sessionnotinitialisederror) */
SessionNotInitialised = 6001,
/** The session is already initialised.
*
* [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.
*
* [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.
*
* [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.
*
* [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.
*
* [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 is returned in `payload.editionName`.
*
* [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.
*
* [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.
*
* [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.
*
* [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.
*
* [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.
*
* 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) */
LibraryDownload = 8006,
/** 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) */
LocalLibraryNotFound = 8007,
/** 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) */
LibraryNotResolved = 8008,
/** 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) */
InvalidLibraryName = 8009,
/** 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) */
DependencyDiscovery = 8010,
/** 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) */
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.
*
* [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.
*
* [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.
*
* [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#refactoringnotsupported) */
RefactoringNotSupported = 9003,
}

View File

@ -6,13 +6,14 @@ import GraphNode from '@/components/GraphNode.vue'
import TopBar from '@/components/TopBar.vue'
import { useGraphStore } from '@/stores/graph'
import { useProjectStore } from '@/stores/project'
import { ExecutionContext, useProjectStore } from '@/stores/project'
import type { Rect } from '@/stores/rect'
import { modKey, useWindowEvent } from '@/util/events'
import { useNavigator } from '@/util/navigator'
import type { Opt } from '@/util/opt'
import { Vec2 } from '@/util/vec2'
import type { ContentRange, ExprId } from 'shared/yjsModel'
import { reactive, ref } from 'vue'
import { onMounted, onUnmounted, reactive, ref, shallowRef } from 'vue'
const EXECUTION_MODES = ['design', 'live']
@ -22,12 +23,23 @@ const viewportNode = ref<HTMLElement>()
const navigator = useNavigator(viewportNode)
const graphStore = useGraphStore()
const projectStore = useProjectStore()
const executionCtx = shallowRef<ExecutionContext>()
const componentBrowserVisible = ref(false)
const componentBrowserPosition = ref(Vec2.Zero())
const nodeRects = reactive(new Map<ExprId, Rect>())
const exprRects = reactive(new Map<ExprId, Rect>())
onMounted(async () => {
const executionCtxPromise = projectStore.createExecutionContextForMain()
onUnmounted(async () => {
executionCtx.value = undefined
const ctx = await executionCtxPromise
if (ctx != null) ctx.destroy()
})
executionCtx.value = (await executionCtxPromise) ?? undefined
})
function updateNodeRect(id: ExprId, rect: Rect) {
nodeRects.set(id, rect)
}

View File

@ -6,6 +6,7 @@ export interface GuiConfig {
preferredVersion?: string
rpcUrl?: string
dataUrl?: string
namespace?: string
}
startup?: {
project?: string

View File

@ -1,11 +1,14 @@
import { useGuiConfig, type GuiConfig } from '@/providers/guiConfig'
import { attachProvider } from '@/util/crdt'
import type { Opt } from '@/util/opt'
import { Client, RequestManager, WebSocketTransport } from '@open-rpc/client-js'
import { computedAsync } from '@vueuse/core'
import * as random from 'lib0/random'
import { defineStore } from 'pinia'
import { LanguageServer } from 'shared/languageServer'
import { DistributedProject } from 'shared/yjsModel'
import { markRaw, ref, watchEffect } from 'vue'
import type { ContentRoot, ContextId, MethodPointer } from 'shared/languageServerTypes'
import { DistributedProject, type Uuid } from 'shared/yjsModel'
import { computed, markRaw, ref, watchEffect } from 'vue'
import { Awareness } from 'y-protocols/awareness'
import * as Y from 'yjs'
@ -28,6 +31,58 @@ function resolveLsUrl(config: GuiConfig): LsUrls {
throw new Error('Incomplete engine configuration')
}
async function initializeLsRpcConnection(urls: LsUrls): Promise<{
connection: LanguageServer
contentRoots: ContentRoot[]
}> {
const transport = new WebSocketTransport(urls.rpcUrl)
const requestManager = new RequestManager([transport])
const client = new Client(requestManager)
const clientId = random.uuidv4() as Uuid
const connection = new LanguageServer(client)
const contentRoots = (await connection.initProtocolConnection(clientId)).contentRoots
return { connection, contentRoots }
}
/**
* Execution Context
*
* This class represent an execution context created in the Language Server. It creates
* it and pushes the initial frame upon construction.
*
* It hides the asynchronous nature of the language server. Each call is scheduled and
* run only when the previous call is done.
*/
export class ExecutionContext {
state: Promise<{ lsRpc: LanguageServer; id: ContextId }>
constructor(
lsRpc: Promise<LanguageServer>,
call: {
methodPointer: MethodPointer
thisArgumentExpression?: string
positionalArgumentsExpressions?: string[]
},
) {
this.state = lsRpc.then(async (lsRpc) => {
const { contextId } = await lsRpc.createExecutionContext()
await lsRpc.pushExecutionContextItem(contextId, {
type: 'ExplicitCall',
positionalArgumentsExpressions: call.positionalArgumentsExpressions ?? [],
...call,
})
return { lsRpc, id: contextId }
})
}
destroy() {
this.state = this.state.then(({ lsRpc, id }) => {
lsRpc.destroyExecutionContext(id)
return { lsRpc, id }
})
}
}
/**
* The project store synchronizes and holds the open project-related data. The synchronization is
* performed using a CRDT data types from Yjs. Once the data is synchronized with a "LS bridge"
@ -44,14 +99,15 @@ export const useProjectStore = defineStore('project', () => {
if (projectId == null) throw new Error('Missing project ID')
const lsUrls = resolveLsUrl(config.value)
const rpcTransport = new WebSocketTransport(lsUrls.rpcUrl)
const rpcRequestManager = new RequestManager([rpcTransport])
const rpcClient = new Client(rpcRequestManager)
const lsRpcConnection = new LanguageServer(rpcClient)
const initializedConnection = initializeLsRpcConnection(lsUrls)
const lsRpcConnection = initializedConnection.then(({ connection }) => connection)
const contentRoots = initializedConnection.then(({ contentRoots }) => contentRoots)
const undoManager = new Y.UndoManager([], { doc })
const name = computed(() => config.value.startup?.project)
const namespace = computed(() => config.value.engine?.namespace)
watchEffect((onCleanup) => {
// For now, let's assume that the websocket server is running on the same host as the web server.
// Eventually, we can make this configurable, or even runtime variable.
@ -99,11 +155,37 @@ export const useProjectStore = defineStore('project', () => {
})
})
async function createExecutionContextForMain(): Promise<Opt<ExecutionContext>> {
if (name.value == null) {
console.error('Cannot create execution context. Unknown project name.')
return
}
if (namespace.value == null) {
console.warn(
'Unknown project\'s namespace. Assuming "local", however it likely won\'t work in cloud',
)
}
const projectName = `${namespace.value ?? 'local'}.${name.value}`
const mainModule = `${projectName}.Main`
const projectRoot = (await contentRoots).find((root) => root.type === 'Project')
if (projectRoot == null) {
console.error(
'Cannot create execution context. Protocol connection initialization did not return a project root.',
)
return
}
return new ExecutionContext(lsRpcConnection, {
methodPointer: { module: mainModule, definedOnType: mainModule, name: 'main' },
})
}
return {
setObservedFileName(name: string) {
observedFileName.value = name
},
createExecutionContextForMain,
module,
contentRoots,
undoManager,
awareness,
lsRpcConnection: markRaw(lsRpcConnection),

View File

@ -21,8 +21,6 @@ export default defineConfig({
alias: {
shared: fileURLToPath(new URL('./shared', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
// workaround for @open-rpc/client-js bug: https://github.com/open-rpc/client-js/issues/310
events: 'shared/event.ts',
},
},
define: {

View File

@ -1,9 +1,9 @@
import { Client, RequestManager, WebSocketTransport } from '@open-rpc/client-js'
import * as map from 'lib0/map'
import { createMutex } from 'lib0/mutex'
import { ObservableV2 } from 'lib0/observable'
import * as random from 'lib0/random'
import * as Y from 'yjs'
import { Emitter } from '../shared/event'
import { LanguageServer } from '../shared/languageServer'
import { Checksum, Path, response } from '../shared/languageServerTypes'
import { DistributedModule, DistributedProject, NodeMetadata, Uuid } from '../shared/yjsModel'
@ -12,10 +12,10 @@ import { WSSharedDoc } from './ydoc'
const sessions = new Map<string, LanguageServerSession>()
type Events = {
error: [error: Error]
error: (error: Error) => void
}
export class LanguageServerSession extends Emitter<Events> {
export class LanguageServerSession extends ObservableV2<Events> {
clientId: Uuid
indexDoc: WSSharedDoc
docs: Map<string, WSSharedDoc>
@ -182,7 +182,7 @@ const pushPathSegment = (path: Path, segment: string): Path => {
type Mutex = ReturnType<typeof createMutex>
class ModulePersistence extends Emitter<{ removed: [] }> {
class ModulePersistence extends ObservableV2<{ removed: () => void }> {
ls: LanguageServer
model: DistributedModule
path: Path

View File

@ -9,8 +9,8 @@ import * as Y from 'yjs'
import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding'
import { ObservableV2 } from 'lib0/observable.js'
import { WebSocket } from 'ws'
import { Emitter } from '../shared/event'
import { LanguageServerSession } from './languageServerSession'
const pingTimeout = 30000
@ -113,7 +113,7 @@ export function setupGatewayClient(ws: WebSocket, lsUrl: string, docName: string
})
}
class YjsConnection extends Emitter<{ close: [] }> {
class YjsConnection extends ObservableV2<{ close: () => void }> {
ws: WebSocket
wsDoc: WSSharedDoc
constructor(ws: WebSocket, wsDoc: WSSharedDoc) {

9
package-lock.json generated
View File

@ -33,6 +33,7 @@
"@vueuse/core": "^10.4.1",
"codemirror": "^6.0.1",
"enso-authentication": "^1.0.0",
"events": "^3.3.0",
"hash-sum": "^2.0.0",
"install": "^0.13.0",
"isomorphic-ws": "^5.0.0",
@ -7718,6 +7719,14 @@
"node": ">=0.10.0"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/execa": {
"version": "7.2.0",
"dev": true,