diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..213bcb422 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "useTabs": false, + "tabWidth": 4, + "quoteProps": "consistent" + } + \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..cc28db98e --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: npm run start:example:server:python \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index b9de751d8..96c772d81 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -105,7 +105,7 @@ export default [{ '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-wrapper-object-types': 'error', - '@stylistic/indent': 'error', + '@stylistic/indent': ['error', 4], '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-misused-new': 'error', '@typescript-eslint/no-namespace': 'off', diff --git a/package-lock.json b/package-lock.json index deabdedf9..e5dd7728d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2413,6 +2413,18 @@ "@codingame/monaco-vscode-view-title-bar-service-override": "14.0.4" } }, + "node_modules/@codingame/monaco-vscode-working-copy-service-override": { + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-working-copy-service-override/-/monaco-vscode-working-copy-service-override-14.0.4.tgz", + "integrity": "sha512-m3Y9sMMQ+pX9l9wN/8pbMXrCpwWKFXtOjvfFhe2ZsvRxPFknlQOUN6lgh5xPtdE1WGBmRzgqIYyoxi+W6io4iA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-99f24462-c56d-5407-83fb-2ea9dd33cc8c-common": "14.0.4", + "@codingame/monaco-vscode-api": "14.0.4", + "@codingame/monaco-vscode-ce8c4b18-4e75-55dd-9656-517347af9de7-common": "14.0.4", + "@codingame/monaco-vscode-files-service-override": "14.0.4" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -11295,6 +11307,7 @@ "@codingame/monaco-vscode-localization-service-override": "~14.0.4", "@codingame/monaco-vscode-log-service-override": "~14.0.4", "@codingame/monaco-vscode-model-service-override": "~14.0.4", + "@codingame/monaco-vscode-working-copy-service-override": "~14.0.4", "vscode": "npm:@codingame/monaco-vscode-extension-api@~14.0.4", "vscode-languageclient": "~9.0.1" }, diff --git a/packages/client/package.json b/packages/client/package.json index bf56288c0..7dbaa870e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -86,6 +86,7 @@ "@codingame/monaco-vscode-localization-service-override": "~14.0.4", "@codingame/monaco-vscode-log-service-override": "~14.0.4", "@codingame/monaco-vscode-model-service-override": "~14.0.4", + "@codingame/monaco-vscode-working-copy-service-override": "~14.0.4", "vscode": "npm:@codingame/monaco-vscode-extension-api@~14.0.4", "vscode-languageclient": "~9.0.1" }, diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index eaaf9cad8..6e8c428a4 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -3,24 +3,35 @@ * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ -import { BaseLanguageClient, MessageTransports, type LanguageClientOptions } from 'vscode-languageclient/browser.js'; +import { + BaseLanguageClient, + MessageTransports, + type LanguageClientOptions, +} from 'vscode-languageclient/browser.js'; export type MonacoLanguageClientOptions = { name: string; id?: string; clientOptions: LanguageClientOptions; messageTransports: MessageTransports; -} +}; export class MonacoLanguageClient extends BaseLanguageClient { - protected readonly messageTransports: MessageTransports; + public readonly messageTransports: MessageTransports; - constructor({ id, name, clientOptions, messageTransports }: MonacoLanguageClientOptions) { + constructor({ + id, + name, + clientOptions, + messageTransports, + }: MonacoLanguageClientOptions) { super(id ?? name.toLowerCase(), name, clientOptions); this.messageTransports = messageTransports; } - protected override createMessageTransports(_encoding: string): Promise { + protected override createMessageTransports( + _encoding: string, + ): Promise { return Promise.resolve(this.messageTransports); } } diff --git a/packages/examples/package.json b/packages/examples/package.json index b39bfa94d..179a7df71 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -140,7 +140,7 @@ "build:msg": "echo Building main examples:", "build": "npm run build:msg && npm run clean && npm run resources:download && npm run extract:docker && npm run compile", "start:server:json": "vite-node src/json/server/direct.ts", - "start:server:python": "vite-node src/python/server/direct.ts", + "start:server:python": "npx vite-node src/python/server/direct.ts", "langium:generate": "langium generate --file ./src/langium/statemachine/config/langium-config.json", "extract:docker": "vite-node ./resources/clangd/scripts/extractDockerFiles.ts", "production:clean": "vite-node ../../scripts/clean.ts --relativePath packages/examples --recursive --paths production", diff --git a/packages/examples/resources/python/hello.py b/packages/examples/resources/python/hello.py deleted file mode 100644 index 0f8a04773..000000000 --- a/packages/examples/resources/python/hello.py +++ /dev/null @@ -1,5 +0,0 @@ -# if hello2 is not resolved the file is not availale on the language server -from hello2 import print_hello - -print_hello() -print("Hello Moon!") diff --git a/packages/examples/resources/python/main.py b/packages/examples/resources/python/main.py new file mode 100644 index 000000000..76be8af17 --- /dev/null +++ b/packages/examples/resources/python/main.py @@ -0,0 +1,28 @@ +# if hello2 is not resolved the file is not availale on the language server +from hello2 import print_hello +import numpy as np + +print_hello() +print("Hello Moon!") + +# Create a NumPy array +arr = np.array([1, 2, 3, 4, 5]) +print("Original array:", arr) + +# Perform element-wise addition +arr_added = arr + 5 +print("Array after adding 5 to each element:", arr_added) + +# Calculate the mean of the array +mean_value = np.mean(arr) +print("Mean of the array:", mean_value) + +# Reshape the array +reshaped_arr = arr.reshape((5, 1)) +print("Reshaped array (5x1):\n", reshaped_arr) + +# Perform matrix multiplication +matrix1 = np.array([[1, 2], [3, 4]]) +matrix2 = np.array([[5, 6], [7, 8]]) +result_matrix = np.dot(matrix1, matrix2) +print("Result of matrix multiplication:\n", result_matrix) \ No newline at end of file diff --git a/packages/examples/resources/python/requirements.txt b/packages/examples/resources/python/requirements.txt new file mode 100644 index 000000000..66d0fa37b --- /dev/null +++ b/packages/examples/resources/python/requirements.txt @@ -0,0 +1 @@ +numpy==2.2.3 \ No newline at end of file diff --git a/packages/examples/resources/styles/views.css b/packages/examples/resources/styles/views.css index 695b7eec9..1ebf73000 100644 --- a/packages/examples/resources/styles/views.css +++ b/packages/examples/resources/styles/views.css @@ -66,7 +66,7 @@ } #workbench-container { - height: 95vh; + height: 100%; display: flex; flex-direction: column } diff --git a/packages/examples/src/appPlayground/config.ts b/packages/examples/src/appPlayground/config.ts index 86a7e53be..fcacc1859 100644 --- a/packages/examples/src/appPlayground/config.ts +++ b/packages/examples/src/appPlayground/config.ts @@ -5,7 +5,11 @@ import * as vscode from 'vscode'; import { LogLevel } from '@codingame/monaco-vscode-api'; -import { RegisteredFileSystemProvider, registerFileSystemOverlay, RegisteredMemoryFile } from '@codingame/monaco-vscode-files-service-override'; +import { + RegisteredFileSystemProvider, + registerFileSystemOverlay, + RegisteredMemoryFile, +} from '@codingame/monaco-vscode-files-service-override'; import getKeybindingsServiceOverride from '@codingame/monaco-vscode-keybindings-service-override'; import getLifecycleServiceOverride from '@codingame/monaco-vscode-lifecycle-service-override'; import getLocalizationServiceOverride from '@codingame/monaco-vscode-localization-service-override'; @@ -27,7 +31,10 @@ import '@codingame/monaco-vscode-search-result-default-extension'; import '../../resources/vsix/open-collaboration-tools.vsix'; import { createDefaultLocaleConfiguration } from 'monaco-languageclient/vscode/services'; -import { defaultHtmlAugmentationInstructions, defaultViewsInit } from 'monaco-editor-wrapper/vscode/services'; +import { + defaultHtmlAugmentationInstructions, + defaultViewsInit, +} from 'monaco-editor-wrapper/vscode/services'; import { configureDefaultWorkerFactory } from 'monaco-editor-wrapper/workers/workerLoaders'; import { createDefaultWorkspaceFile } from '../common/client/utils.js'; import helloTsCode from '../../resources/appPlayground/hello.ts?raw'; @@ -35,14 +42,16 @@ import testerTsCode from '../../resources/appPlayground/tester.ts?raw'; import type { WrapperConfig } from 'monaco-editor-wrapper'; export type ConfigResult = { - wrapperConfig: WrapperConfig + wrapperConfig: WrapperConfig; workspaceFile: vscode.Uri; helloTsUri: vscode.Uri; testerTsUri: vscode.Uri; }; export const configure = (htmlContainer?: HTMLElement): ConfigResult => { - const workspaceFile = vscode.Uri.file('/workspace/.vscode/workspace.code-workspace'); + const workspaceFile = vscode.Uri.file( + '/workspace/.vscode/workspace.code-workspace', + ); const wrapperConfig: WrapperConfig = { $type: 'extended', @@ -53,7 +62,9 @@ export const configure = (htmlContainer?: HTMLElement): ConfigResult => { serviceOverrides: { ...getKeybindingsServiceOverride(), ...getLifecycleServiceOverride(), - ...getLocalizationServiceOverride(createDefaultLocaleConfiguration()), + ...getLocalizationServiceOverride( + createDefaultLocaleConfiguration(), + ), ...getBannerServiceOverride(), ...getStatusBarServiceOverride(), ...getTitleBarServiceOverride(), @@ -61,21 +72,26 @@ export const configure = (htmlContainer?: HTMLElement): ConfigResult => { ...getRemoteAgentServiceOverride(), ...getEnvironmentServiceOverride(), ...getSecretStorageServiceOverride(), - ...getStorageServiceOverride(), - ...getSearchServiceOverride() + ...getStorageServiceOverride({ + fallbackOverride: { + 'workbench.activity.showAccounts': false, + }, + }), + ...getSearchServiceOverride(), }, enableExtHostWorker: true, viewsConfig: { viewServiceType: 'ViewsService', - htmlAugmentationInstructions: defaultHtmlAugmentationInstructions, - viewsInitFunc: defaultViewsInit + htmlAugmentationInstructions: + defaultHtmlAugmentationInstructions, + viewsInitFunc: defaultViewsInit, }, workspaceConfig: { enableWorkspaceTrust: true, windowIndicator: { label: 'mlc-app-playground', tooltip: '', - command: '' + command: '', }, workspaceProvider: { trusted: true, @@ -84,56 +100,67 @@ export const configure = (htmlContainer?: HTMLElement): ConfigResult => { return true; }, workspace: { - workspaceUri: workspaceFile - } + workspaceUri: workspaceFile, + }, }, configurationDefaults: { - 'window.title': 'mlc-app-playground${separator}${dirty}${activeEditorShort}' + 'window.title': + 'mlc-app-playground${separator}${dirty}${activeEditorShort}', }, productConfiguration: { nameShort: 'mlc-app-playground', - nameLong: 'mlc-app-playground' - } + nameLong: 'mlc-app-playground', + }, }, userConfiguration: { json: JSON.stringify({ 'workbench.colorTheme': 'Default Dark Modern', 'editor.wordBasedSuggestions': 'off', - 'typescript.tsserver.web.projectWideIntellisense.enabled': true, - 'typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors': false, + 'typescript.tsserver.web.projectWideIntellisense.enabled': + true, + 'typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors': + false, 'editor.guides.bracketPairsHorizontal': true, 'oct.serverUrl': 'https://api.open-collab.tools/', - 'editor.experimental.asyncTokenization': false - }) + 'editor.experimental.asyncTokenization': false, + }), }, }, - extensions: [{ - config: { - name: 'mlc-app-playground', - publisher: 'TypeFox', - version: '1.0.0', - engines: { - vscode: '*' - } - } - }], + extensions: [ + { + config: { + name: 'mlc-app-playground', + publisher: 'TypeFox', + version: '1.0.0', + engines: { + vscode: '*', + }, + }, + }, + ], editorAppConfig: { - monacoWorkerFactory: configureDefaultWorkerFactory - } + monacoWorkerFactory: configureDefaultWorkerFactory, + }, }; const helloTsUri = vscode.Uri.file('/workspace/hello.ts'); const testerTsUri = vscode.Uri.file('/workspace/tester.ts'); const fileSystemProvider = new RegisteredFileSystemProvider(false); - fileSystemProvider.registerFile(new RegisteredMemoryFile(helloTsUri, helloTsCode)); - fileSystemProvider.registerFile(new RegisteredMemoryFile(testerTsUri, testerTsCode)); - fileSystemProvider.registerFile(createDefaultWorkspaceFile(workspaceFile, '/workspace')); + fileSystemProvider.registerFile( + new RegisteredMemoryFile(helloTsUri, helloTsCode), + ); + fileSystemProvider.registerFile( + new RegisteredMemoryFile(testerTsUri, testerTsCode), + ); + fileSystemProvider.registerFile( + createDefaultWorkspaceFile(workspaceFile, '/workspace'), + ); registerFileSystemOverlay(1, fileSystemProvider); return { wrapperConfig, workspaceFile, helloTsUri, - testerTsUri + testerTsUri, }; }; diff --git a/packages/examples/src/common/node/server-commons.ts b/packages/examples/src/common/node/server-commons.ts index ddc6f8d69..4e7b1dfee 100644 --- a/packages/examples/src/common/node/server-commons.ts +++ b/packages/examples/src/common/node/server-commons.ts @@ -9,15 +9,30 @@ import { Socket } from 'node:net'; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import * as cp from 'node:child_process'; -import { type IWebSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'; -import { createConnection, createServerProcess, forward } from 'vscode-ws-jsonrpc/server'; -import { Message, InitializeRequest, type InitializeParams, type RequestMessage, type ResponseMessage } from 'vscode-languageserver-protocol'; +import { + type IWebSocket, + WebSocketMessageReader, + WebSocketMessageWriter, +} from 'vscode-ws-jsonrpc'; +import { + createConnection, + createServerProcess, + forward, +} from 'vscode-ws-jsonrpc/server'; +import { + Message, + InitializeRequest, + type InitializeParams, + type RequestMessage, + type ResponseMessage, +} from 'vscode-languageserver-protocol'; +import * as fs from 'node:fs'; export enum LanguageName { /** https://nodejs.org/api/cli.html */ node = 'node', /** https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html */ - java = 'java' + java = 'java', } export interface LanguageServerRunConfig { @@ -26,7 +41,7 @@ export interface LanguageServerRunConfig { serverPort: number; runCommand: LanguageName | string; runCommandArgs: string[]; - wsServerOptions: ServerOptions, + wsServerOptions: ServerOptions; spawnOptions?: cp.SpawnOptions; logMessages?: boolean; requestMessageHandler?: (message: RequestMessage) => RequestMessage; @@ -36,23 +51,112 @@ export interface LanguageServerRunConfig { /** * start the language server inside the current process */ -export const launchLanguageServer = (runconfig: LanguageServerRunConfig, socket: IWebSocket) => { +export const launchLanguageServer = ( + runconfig: LanguageServerRunConfig, + socket: IWebSocket, +) => { const { serverName, runCommand, runCommandArgs, spawnOptions } = runconfig; // start the language server as an external process const reader = new WebSocketMessageReader(socket); const writer = new WebSocketMessageWriter(socket); - const socketConnection = createConnection(reader, writer, () => socket.dispose()); - const serverConnection = createServerProcess(serverName, runCommand, runCommandArgs, spawnOptions); + const socketConnection = createConnection(reader, writer, () => + socket.dispose(), + ); + const serverConnection = createServerProcess( + serverName, + runCommand, + runCommandArgs, + spawnOptions, + ); if (serverConnection !== undefined) { - forward(socketConnection, serverConnection, message => { + forward(socketConnection, serverConnection, (message) => { if (Message.isRequest(message)) { + if (message.method === 'fileSync') { + const fileSyncParams = message.params as { + path: string[]; + text: string; + updated: number; + }; + + try { + const path = `/${fileSyncParams.path.join('/')}`; + if (fs.existsSync(path)) { + const mtime = fs.statSync(path).mtimeMs; + if (fileSyncParams.updated >= mtime) { + fs.writeFileSync(path, fileSyncParams.text); + fs.utimesSync( + path, + new Date(fileSyncParams.updated), + new Date(fileSyncParams.updated), + ); + } + } else { + const pathParent = `/${fileSyncParams.path + .slice(0, -1) + .join('/')}`; + fs.mkdirSync(pathParent, { recursive: true }); + fs.writeFileSync(path, fileSyncParams.text); + } + } catch (error) { + console.error( + '[FILE] cannot update', + fileSyncParams, + error, + ); + } + + return message; + } + + if (message.method === 'fileDelete') { + const fileDeleteParams = message.params as { + path: string[]; + }; + + try { + const path = `/${fileDeleteParams.path.join('/')}`; + fs.rmSync(path, { recursive: true }); + return message; + } catch (error) { + console.error( + '[FILE] cannot delete', + fileDeleteParams, + error, + ); + } + } + + if (message.method === 'pipInstall') { + const pipInstallParams = message.params as { + path: string[]; + }; + + try { + const path = `/${pipInstallParams.path.join('/')}`; + cp.execSync(`python -m venv ${path}/venv`); + cp.execSync( + `${path}/venv/bin/python -m pip install -r requirements.txt`, + { cwd: path }, + ); + } catch (error) { + console.error( + '[FILE] cannot pip install', + pipInstallParams, + error, + ); + } + return message; + } + if (message.method === InitializeRequest.type.method) { const initializeParams = message.params as InitializeParams; initializeParams.processId = process.pid; } if (runconfig.logMessages ?? false) { - console.log(`${serverName} Server received: ${message.method}`); + console.log( + `${serverName} Server received: ${message.method}`, + ); console.log(message); } if (runconfig.requestMessageHandler !== undefined) { @@ -73,40 +177,50 @@ export const launchLanguageServer = (runconfig: LanguageServerRunConfig, socket: } }; -export const upgradeWsServer = (runconfig: LanguageServerRunConfig, +export const upgradeWsServer = ( + runconfig: LanguageServerRunConfig, config: { - server: Server, - wss: WebSocketServer - }) => { - config.server.on('upgrade', (request: IncomingMessage, socket: Socket, head: Buffer) => { - const baseURL = `http://${request.headers.host}/`; - const pathName = request.url !== undefined ? new URL(request.url, baseURL).pathname : undefined; - if (pathName === runconfig.pathName) { - config.wss.handleUpgrade(request, socket, head, webSocket => { - const socket: IWebSocket = { - send: content => webSocket.send(content, error => { - if (error) { - throw error; - } - }), - onMessage: cb => webSocket.on('message', (data) => { - cb(data); - }), - onError: cb => webSocket.on('error', cb), - onClose: cb => webSocket.on('close', cb), - dispose: () => webSocket.close() - }; - // launch the server when the web socket is opened - if (webSocket.readyState === webSocket.OPEN) { - launchLanguageServer(runconfig, socket); - } else { - webSocket.on('open', () => { + server: Server; + wss: WebSocketServer; + }, +) => { + config.server.on( + 'upgrade', + (request: IncomingMessage, socket: Socket, head: Buffer) => { + const baseURL = `http://${request.headers.host}/`; + const pathName = + request.url !== undefined + ? new URL(request.url, baseURL).pathname + : undefined; + if (pathName === runconfig.pathName) { + config.wss.handleUpgrade(request, socket, head, (webSocket) => { + const socket: IWebSocket = { + send: (content) => + webSocket.send(content, (error) => { + if (error) { + throw error; + } + }), + onMessage: (cb) => + webSocket.on('message', (data) => { + cb(data); + }), + onError: (cb) => webSocket.on('error', cb), + onClose: (cb) => webSocket.on('close', cb), + dispose: () => webSocket.close(), + }; + // launch the server when the web socket is opened + if (webSocket.readyState === webSocket.OPEN) { launchLanguageServer(runconfig, socket); - }); - } - }); - } - }); + } else { + webSocket.on('open', () => { + launchLanguageServer(runconfig, socket); + }); + } + }); + } + }, + ); }; /** diff --git a/packages/examples/src/debugger/client/debugger.ts b/packages/examples/src/debugger/client/debugger.ts index a759bb90c..565ba1278 100644 --- a/packages/examples/src/debugger/client/debugger.ts +++ b/packages/examples/src/debugger/client/debugger.ts @@ -5,13 +5,15 @@ import * as vscode from 'vscode'; import type { ExtensionConfig } from 'monaco-editor-wrapper'; -import type { ConfigParams, InitMessage } from '../common/definitions.js'; +import type { ConfigParams } from '../common/definitions.js'; // This is derived from: // https://github.com/CodinGame/monaco-vscode-api/blob/main/demo/src/features/debugger.ts // The client configuration is generic and can be used for a another language -export const provideDebuggerExtensionConfig = (config: ConfigParams): ExtensionConfig => { +export const provideDebuggerExtensionConfig = ( + config: ConfigParams, +): ExtensionConfig => { const filesOrContents = new Map(); filesOrContents.set('./extension.js', '// nothing'); @@ -21,7 +23,7 @@ export const provideDebuggerExtensionConfig = (config: ConfigParams): ExtensionC publisher: 'TypeFox', version: '1.0.0', engines: { - vscode: '*' + vscode: '*', }, // A browser field is mandatory for the extension to be flagged as `web` browser: 'extension.js', @@ -30,24 +32,25 @@ export const provideDebuggerExtensionConfig = (config: ConfigParams): ExtensionC { type: config.languageId, label: 'Test', - languages: [config.languageId] - } + languages: [config.languageId], + }, ], breakpoints: [ { - language: config.languageId - } - ] + language: config.languageId, + }, + ], }, - activationEvents: [ - 'onDebug' - ] + activationEvents: ['onDebug'], }, - filesOrContents + filesOrContents, }; }; -export const confiugureDebugging = async (api: typeof vscode, config: ConfigParams) => { +export const configureDebugging = async ( + api: typeof vscode, + config: ConfigParams, +) => { class WebsocketDebugAdapter implements vscode.DebugAdapter { private websocket: WebSocket; @@ -75,32 +78,38 @@ export const confiugureDebugging = async (api: typeof vscode, config: ConfigPara api.debug.registerDebugAdapterDescriptorFactory(config.languageId, { async createDebugAdapterDescriptor() { - const websocket = new WebSocket(`${config.protocol}://${config.hostname}:${config.port}`); + const websocket = new WebSocket( + `${config.protocol}://${config.hostname}:${config.port}`, + ); await new Promise((resolve, reject) => { websocket.onopen = resolve; websocket.onerror = () => - reject(new Error(`Unable to connect to debugger server. Run "${config.helpContainerCmd}"`)); + reject( + new Error( + `Unable to connect to debugger server. Run "${config.helpContainerCmd}"`, + ), + ); }); const adapter = new WebsocketDebugAdapter(websocket); - const initMessage: InitMessage = { - id: 'init', - files: {}, - // the default file is the one that will be used by the debugger - defaultFile: config.defaultFile, - debuggerExecCall: config.debuggerExecCall - }; - for (const [name, fileDef] of config.files.entries()) { - console.log(`Found: ${name} Sending file: ${fileDef.path}`); - initMessage.files[name] = { - path: fileDef.path, - code: fileDef.code, - uri: fileDef.uri - }; - } - websocket.send(JSON.stringify(initMessage)); + // const initMessage: InitMessage = { + // id: 'init', + // files: {}, + // // the default file is the one that will be used by the debugger + // defaultFile: config.defaultFile.path, + // debuggerExecCall: config.debuggerExecCall, + // }; + // for (const [name, fileDef] of config.files.entries()) { + // console.log(`Found: ${name} Sending file: ${fileDef.path}`); + // initMessage.files[name] = { + // path: fileDef.path, + // code: fileDef.code, + // uri: fileDef.uri, + // }; + // } + // websocket.send(JSON.stringify(initMessage)); // eslint-disable-next-line @typescript-eslint/no-explicit-any adapter.onDidSendMessage((message: any) => { @@ -109,6 +118,6 @@ export const confiugureDebugging = async (api: typeof vscode, config: ConfigPara } }); return new api.DebugAdapterInlineImplementation(adapter); - } + }, }); }; diff --git a/packages/examples/src/debugger/common/definitions.ts b/packages/examples/src/debugger/common/definitions.ts index 348434141..4e0edd5bf 100644 --- a/packages/examples/src/debugger/common/definitions.ts +++ b/packages/examples/src/debugger/common/definitions.ts @@ -5,16 +5,17 @@ import { RegisteredMemoryFile } from '@codingame/monaco-vscode-files-service-override'; import { Uri } from 'vscode'; +import type { ServerSyncingFileSystemProvider } from './serverSyncingFileSystemProvider.js'; export type FileDefinition = { path: string; code: string; uri: Uri; -} +}; export type InitMessage = { - id: 'init', - files: Record + id: 'init'; + files: Record; defaultFile: string; debuggerExecCall: string; }; @@ -30,13 +31,16 @@ export type ConfigParams = { protocol: 'ws' | 'wss'; hostname: string; port: number; - files: Map; - defaultFile: string; + fileSystemProvider: ServerSyncingFileSystemProvider; + defaultFile: Uri | null; helpContainerCmd: string; debuggerExecCall: string; -} +}; -export const createDebugLaunchConfigFile = (workspacePath: string, type: string) => { +export const createDebugLaunchConfigFile = ( + workspacePath: string, + type: string, +) => { return new RegisteredMemoryFile( Uri.file(`${workspacePath}/.vscode/launch.json`), JSON.stringify( @@ -47,12 +51,11 @@ export const createDebugLaunchConfigFile = (workspacePath: string, type: string) name: 'Debugger: Lauch', type, request: 'attach', - } - ] + }, + ], }, null, - 2 - ) + 2, + ), ); }; - diff --git a/packages/examples/src/debugger/common/serverSyncingFileSystemProvider.ts b/packages/examples/src/debugger/common/serverSyncingFileSystemProvider.ts new file mode 100644 index 000000000..638640fbd --- /dev/null +++ b/packages/examples/src/debugger/common/serverSyncingFileSystemProvider.ts @@ -0,0 +1,312 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ +/* eslint-disable @stylistic/indent */ + +import { type IDisposable } from '@codingame/monaco-vscode-api/vscode/vs/base/common/lifecycle'; +import { URI } from '@codingame/monaco-vscode-api/vscode/vs/base/common/uri'; +import { Emitter } from '@codingame/monaco-vscode-editor-api'; +import { + FileSystemProviderCapabilities, + FileSystemProviderError, + FileSystemProviderErrorCode, + FileType, + type IFileChange, + type IFileDeleteOptions, + type IFileOverwriteOptions, + type IFileSystemProviderWithFileReadWriteCapability, + type IFileWriteOptions, + type IStat, + type IWatchOptions, +} from '@codingame/monaco-vscode-files-service-override'; +import { type Event as VSEvent } from 'vscode'; + +export interface FileText { + text: string; + updated: number; +} + +export interface Files { + [path: string]: Files | FileText; +} + +export function isText(obj: unknown): obj is Text { + const typedObj = obj as Text; + return typeof typedObj === 'string'; +} + +export function isFileText(obj: object): obj is FileText { + const typedObj = obj as FileText; + return isText(typedObj.text); +} + +export interface FlatFile { + path: string[]; + text: string; + updated: number; +} + +export class ServerSyncingFileSystemProvider + implements IFileSystemProviderWithFileReadWriteCapability +{ + capabilities: FileSystemProviderCapabilities; + + _onDidChangeCapabilities = new Emitter(); + onDidChangeCapabilities = this._onDidChangeCapabilities.event; + + _onDidChangeFile = new Emitter(); + onDidChangeFile = this._onDidChangeFile.event; + + _onDidChangeOverlays = new Emitter(); + onDidChangeOverlays = this._onDidChangeOverlays.event; + + onFileUpdate: (file: FlatFile) => Promise; + onFileDelete: (path: string[]) => Promise; + + onDidWatchError?: VSEvent | undefined; + + root: Files; + + constructor( + root: Files, + onFileUpdate: (file: FlatFile) => Promise, + onFileDelete: (path: string[]) => Promise, + ) { + this.root = root; + this.onFileUpdate = onFileUpdate; + this.onFileDelete = onFileDelete; + this.capabilities = + // eslint-disable-next-line no-bitwise + FileSystemProviderCapabilities.FileReadWrite | + FileSystemProviderCapabilities.PathCaseSensitive; + } + + getAllFiles(resource?: URI): FlatFile[] { + const flatFiles: FlatFile[] = []; + + function f(files: Files, path: string[]) { + Object.entries(files).forEach(([key, value]) => { + if (isFileText(value)) { + flatFiles.push({ + path: [...path, key], + text: value.text, + updated: value.updated, + }); + } else { + f(value, [...path, key]); + } + }); + } + + const { file } = resource + ? this.getFile(resource) + : { file: this.root }; + if (!file) { + return []; + } + + if (isFileText(file)) { + throw new Error(); + } + + f(file, []); + + return flatFiles; + } + + getFile( + resource: URI, + create?: 'directory' | 'file', + ): { path: string[]; parent: Files | null; file: Files | FileText | null } { + if (resource.scheme !== 'file') { + console.error('[FILE] only file scheme is supported'); + throw new Error(); + } + + if (!resource.path.startsWith('/')) { + console.error('[FILE] only root paths are allowed'); + throw new Error(); + } + + const paths = resource.path.slice(1).split('/'); + let parent: Files = this.root; + let file: Files | FileText = this.root; + for (let i = 0; i < paths.length; i++) { + const path = paths[i]!; + if (isFileText(file)) { + console.error(`[FILE] ${path} is not a directory`); + throw new Error(); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (file[path] === undefined) { + if (create) { + if (i < paths.length - 1 || create === 'directory') { + file[path] = {}; + } else { + file[path] = { + text: '', + updated: Date.now(), + }; + } + } else { + return { path: paths, parent: null, file: null }; + } + } + + parent = file; + file = file[path]; + } + + return { path: paths, parent, file }; + } + + readFile(resource: URI): Promise { + const { file } = this.getFile(resource); + if (!file || !isFileText(file)) { + console.error(`[FILE] ${resource.path} does not exist`); + throw new Error(); + } + + return Promise.resolve(new TextEncoder().encode(file.text)); + } + + async writeFile( + resource: URI, + content: Uint8Array, + _opts: IFileWriteOptions, + ): Promise { + console.log(`[FILE] writeFile ${resource.path}`); + const { path, file } = this.getFile(resource, 'file'); + if (!file || !isFileText(file)) { + throw new Error(); + } + + file.text = new TextDecoder().decode(content); + file.updated = Date.now(); + + await this.onFileUpdate({ + path: path, + text: file.text, + updated: file.updated, + }); + } + + watch(_resource: URI, _opts: IWatchOptions): IDisposable { + return { + dispose: () => { + // + }, + }; + } + + stat(resource: URI): Promise { + const { file } = this.getFile(resource); + if (!file) { + throw FileSystemProviderError.create( + 'Not found', + FileSystemProviderErrorCode.FileNotFound, + ); + } else if (isFileText(file)) { + const stat: IStat = { + type: FileType.File, + mtime: 0, + ctime: 0, + size: file.text.length, + }; + return Promise.resolve(stat); + } else { + const stat: IStat = { + type: FileType.Directory, + mtime: 0, + ctime: 0, + size: 0, + }; + return Promise.resolve(stat); + } + } + + mkdir(resource: URI): Promise { + console.log(`[FILE] mkdir ${resource.path}`); + const { file } = this.getFile(resource, 'directory'); + if (!file || isFileText(file)) { + throw new Error(); + } + return Promise.resolve(); + } + + readdir(resource: URI): Promise> { + const { file } = this.getFile(resource); + if (!file || isFileText(file)) { + throw new Error(); + } + + return Promise.resolve( + Object.entries(file).map((x) => { + return [ + x[0], + isFileText(x[1]) ? FileType.File : FileType.Directory, + ]; + }), + ); + } + + async delete(resource: URI, opts: IFileDeleteOptions): Promise { + console.log('[FILE] delete', resource, opts); + const { path, parent } = this.getFile(resource); + if (parent) { + delete parent[path[path.length - 1]!]; + } + await this.onFileDelete(path); + } + + async rename( + from: URI, + to: URI, + opts: IFileOverwriteOptions, + ): Promise { + console.error('[FILE] rename', from, to, opts); + const { file } = this.getFile(from); + if (!file) { + return; + } + + if (isFileText(file)) { + await this.writeFile(to, new TextEncoder().encode(file.text), { + create: true, + overwrite: true, + unlock: false, + atomic: false, + }); + + await this.delete(from, { + recursive: false, + useTrash: false, + atomic: false, + }); + } else { + const files = this.getAllFiles(from); + + const toFile = this.getFile(to, 'directory'); + for (const file of files) { + await this.writeFile( + URI.file([...toFile.path, file.path].join('/')), + new TextEncoder().encode(file.text), + { + create: true, + overwrite: true, + unlock: false, + atomic: false, + }, + ); + } + + await this.delete(from, { + recursive: false, + useTrash: false, + atomic: false, + }); + } + } +} diff --git a/packages/examples/src/debugger/server/debugServer.ts b/packages/examples/src/debugger/server/debugServer.ts index a9770c334..2aca7efca 100644 --- a/packages/examples/src/debugger/server/debugServer.ts +++ b/packages/examples/src/debugger/server/debugServer.ts @@ -31,8 +31,8 @@ const server = http.createServer(app); const wss = new WebSocketServer({ server }); const sequential = ( - fn: (...params: P) => Promise -): (...params: P) => Promise => { + fn: (...params: P) => Promise, +): ((...params: P) => Promise) => { let promise = Promise.resolve(); return (...params: P) => { const result = promise.then(() => { @@ -40,8 +40,8 @@ const sequential = ( }); promise = result.then( - () => { }, - () => { } + () => {}, + () => {}, ); return result; }; @@ -66,24 +66,36 @@ wss.on('connection', (ws) => { const initMesssage = parsed as InitMessage; const defaultFile = initMesssage.defaultFile; const debuggerExecCall = initMesssage.debuggerExecCall; - for (const [name, fileDef] of Object.entries(initMesssage.files)) { - console.log(`Found file: ${name} path: ${fileDef.path}`); - await fs.promises.writeFile(fileDef.path, fileDef.code); + for (const [name, fileDef] of Object.entries( + initMesssage.files, + )) { + console.log( + `Found file: ${name} path: ${fileDef.path}`, + ); + await fs.promises.writeFile( + fileDef.path, + fileDef.code, + ); } initialized = true; - console.log(`Using default file "${defaultFile}" for debugging.`); + console.log( + `Using default file "${defaultFile}" for debugging.`, + ); - const sendOutput = (category: 'stdout' | 'stderr', output: string | null | undefined) => { + const sendOutput = ( + category: 'stdout' | 'stderr', + output: string | null | undefined, + ) => { onWsMessage( JSON.stringify({ type: 'event', event: 'output', body: { category, - output - } - }) + output, + }, + }), ); }; @@ -103,7 +115,9 @@ wss.on('connection', (ws) => { ws.close(); }); - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => + setTimeout(resolve, 1000), + ); // 4711 is the default port of the GraalPy debugger socket.connect(4711); return; @@ -113,7 +127,7 @@ wss.on('connection', (ws) => { } } socket.sendMessage(message); - }) + }), ); }); diff --git a/packages/examples/src/multi/config.ts b/packages/examples/src/multi/config.ts index 1bb327820..81975c243 100644 --- a/packages/examples/src/multi/config.ts +++ b/packages/examples/src/multi/config.ts @@ -42,7 +42,7 @@ export const createPythonLanguageClientConfig: () => LanguageClientConfig = () = setTimeout(() => { ['pyright.restartserver', 'pyright.organizeimports'].forEach((cmdName) => { vscode.commands.registerCommand(cmdName, (...args: unknown[]) => { - languageClient?.sendRequest('workspace/executeCommand', { command: cmdName, arguments: args }); + void languageClient?.sendRequest('workspace/executeCommand', { command: cmdName, arguments: args }); }); }); }, 250); diff --git a/packages/examples/src/python/client/config.ts b/packages/examples/src/python/client/config.ts index 60e8ce2cb..99b1b833a 100644 --- a/packages/examples/src/python/client/config.ts +++ b/packages/examples/src/python/client/config.ts @@ -19,56 +19,81 @@ import getSearchServiceOverride from '@codingame/monaco-vscode-search-service-ov import getDebugServiceOverride from '@codingame/monaco-vscode-debug-service-override'; import getTestingServiceOverride from '@codingame/monaco-vscode-testing-service-override'; import getPreferencesServiceOverride from '@codingame/monaco-vscode-preferences-service-override'; -import { RegisteredFileSystemProvider, RegisteredMemoryFile, registerFileSystemOverlay } from '@codingame/monaco-vscode-files-service-override'; +import getWorkingCopyServiceOverride from '@codingame/monaco-vscode-working-copy-service-override'; +import { registerFileSystemOverlay } from '@codingame/monaco-vscode-files-service-override'; import '@codingame/monaco-vscode-python-default-extension'; import { LogLevel } from '@codingame/monaco-vscode-api'; import { MonacoLanguageClient } from 'monaco-languageclient'; import { createUrl } from 'monaco-languageclient/tools'; import { createDefaultLocaleConfiguration } from 'monaco-languageclient/vscode/services'; -import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'; -import type { WrapperConfig } from 'monaco-editor-wrapper'; -import { defaultHtmlAugmentationInstructions, defaultViewsInit } from 'monaco-editor-wrapper/vscode/services'; +import { + toSocket, + WebSocketMessageReader, + WebSocketMessageWriter, +} from 'vscode-ws-jsonrpc'; +import type { + MonacoEditorLanguageClientWrapper, + WrapperConfig, +} from 'monaco-editor-wrapper'; +import { + defaultHtmlAugmentationInstructions, + defaultViewsInit, +} from 'monaco-editor-wrapper/vscode/services'; import { configureDefaultWorkerFactory } from 'monaco-editor-wrapper/workers/workerLoaders'; -import { createDefaultWorkspaceFile } from '../../common/client/utils.js'; -import { provideDebuggerExtensionConfig } from '../../debugger/client/debugger.js'; -import helloPyCode from '../../../resources/python/hello.py?raw'; -import hello2PyCode from '../../../resources/python/hello2.py?raw'; -import badPyCode from '../../../resources/python/bad.py?raw'; -import { createDebugLaunchConfigFile, type ConfigParams, type FileDefinition } from '../../debugger/common/definitions.js'; - -export const createDefaultConfigParams = (homeDir: string, htmlContainer?: HTMLElement): ConfigParams => { - const files = new Map(); - const workspaceRoot = `${homeDir}/workspace`; +import { + configureDebugging, + provideDebuggerExtensionConfig, +} from '../../debugger/client/debugger.js'; +import { type ConfigParams } from '../../debugger/common/definitions.js'; +import { + isFileText, + ServerSyncingFileSystemProvider, + type Files, + type FlatFile, +} from '../../debugger/common/serverSyncingFileSystemProvider.js'; +import type { RegisterLocalProcessExtensionResult } from '@codingame/monaco-vscode-api/extensions'; + +export const createDefaultConfigParams = ( + homeDir: string, + files: Files, + defaultFile: vscode.Uri | null, + htmlContainer: HTMLElement, + onFileUpdate: (file: FlatFile) => Promise, + onFileDelete: (path: string[]) => Promise, +): ConfigParams => { + const fileSystemProvider = new ServerSyncingFileSystemProvider( + files, + onFileUpdate, + onFileDelete, + ); + const configParams: ConfigParams = { extensionName: 'debugger-py-client', languageId: 'python', documentSelector: ['python', 'py'], homeDir, workspaceRoot: `${homeDir}/workspace`, - workspaceFile: vscode.Uri.file(`${homeDir}/.vscode/workspace.code-workspace`), + workspaceFile: vscode.Uri.file( + `${homeDir}/.vscode/workspace.code-workspace`, + ), htmlContainer, protocol: 'ws', hostname: 'localhost', port: 55555, - files, - defaultFile: `${workspaceRoot}/hello2.py`, - helpContainerCmd: 'docker compose -f ./packages/examples/resources/debugger/docker-compose.yml up -d', - debuggerExecCall: 'graalpy --dap --dap.WaitAttached --dap.Suspend=true' + fileSystemProvider, + defaultFile: defaultFile, + helpContainerCmd: + 'docker compose -f ./packages/examples/resources/debugger/docker-compose.yml up -d', + debuggerExecCall: 'graalpy --dap --dap.WaitAttached --dap.Suspend=true', }; - const helloPyPath = `${workspaceRoot}/hello.py`; - const hello2PyPath = configParams.defaultFile; - const badPyPath = `${workspaceRoot}/bad.py`; - - files.set('hello.py', { code: helloPyCode, path: helloPyPath, uri: vscode.Uri.file(helloPyPath) }); - files.set('hello2.py', { code: hello2PyCode, path: hello2PyPath, uri: vscode.Uri.file(hello2PyPath) }); - files.set('bad.py', { code: badPyCode, path: badPyPath, uri: vscode.Uri.file(badPyPath) }); - - const fileSystemProvider = new RegisteredFileSystemProvider(false); - fileSystemProvider.registerFile(new RegisteredMemoryFile(files.get('hello.py')!.uri, helloPyCode)); - fileSystemProvider.registerFile(new RegisteredMemoryFile(files.get('hello2.py')!.uri, hello2PyCode)); - fileSystemProvider.registerFile(new RegisteredMemoryFile(files.get('bad.py')!.uri, badPyCode)); - fileSystemProvider.registerFile(createDefaultWorkspaceFile(configParams.workspaceFile, workspaceRoot)); - fileSystemProvider.registerFile(createDebugLaunchConfigFile(workspaceRoot, configParams.languageId)); + + // const fileSystemProvider = new RegisteredFileSystemProvider(false); + // fileSystemProvider.registerFile( + // createDefaultWorkspaceFile(configParams.workspaceFile, workspaceRoot) + // ); + // fileSystemProvider.registerFile( + // createDebugLaunchConfigFile(workspaceRoot, configParams.languageId) + // ); registerFileSystemOverlay(1, fileSystemProvider); return configParams; @@ -77,10 +102,54 @@ export const createDefaultConfigParams = (homeDir: string, htmlContainer?: HTMLE export type PythonAppConfig = { wrapperConfig: WrapperConfig; configParams: ConfigParams; -} + onLoad: (wrapper: MonacoEditorLanguageClientWrapper) => Promise; +}; + +export const createWrapperConfig = (input: { + files: Files; + onFileUpdate: (file: FlatFile) => Promise; + onFileDelete: (path: string[]) => Promise; +}): PythonAppConfig => { + const homeId = String(Date.now()); // TODO: this is a terrible unique id + const homeDir = `/tmp/${homeId}`; + const files: Files = { + tmp: { + [homeId]: { + ['.vscode']: { + ['workspace.code-workspace']: { + updated: Date.now(), + text: JSON.stringify( + { + folders: [ + { + path: `${homeDir}/workspace`, + }, + ], + }, + null, + 2, + ), + }, + }, + ['workspace']: input.files, + }, + }, + }; + const workspacePath = ['tmp', homeId, 'workspace']; -export const createWrapperConfig = (): PythonAppConfig => { - const configParams = createDefaultConfigParams('/home/mlc', document.body); + let defaultFile: vscode.Uri | null; + if ('main.py' in input.files) { + defaultFile = vscode.Uri.file([...workspacePath, 'main.py'].join('/')); + } else { + // Find the first file and show it + const first = Object.entries(files).find((x) => isFileText(x[1]))?.[0]; + + if (first !== undefined) { + defaultFile = vscode.Uri.file([...workspacePath, first].join('/')); + } else { + defaultFile = null; + } + } const url = createUrl({ secured: false, @@ -88,14 +157,114 @@ export const createWrapperConfig = (): PythonAppConfig => { port: 30001, path: 'pyright', extraParams: { - authorization: 'UserAuth' - } + authorization: 'UserAuth', + }, }); + const webSocket = new WebSocket(url); const iWebSocket = toSocket(webSocket); const reader = new WebSocketMessageReader(iWebSocket); const writer = new WebSocketMessageWriter(iWebSocket); + const onFileUpdate = async (file: FlatFile) => { + await writer.write({ + jsonrpc: '2.0', + id: 0, + method: 'fileSync', + params: { path: file.path, text: file.text, updated: file.updated }, + } as { + jsonrpc: string; + }); + + for (let i = 0; i < workspacePath.length; i++) { + if (file.path[i] !== workspacePath[i]) { + return; + } + } + + await input.onFileUpdate({ + path: file.path.slice(workspacePath.length), + text: file.text, + updated: file.updated, + }); + }; + + const onFileDelete = async (path: string[]) => { + await writer.write({ + jsonrpc: '2.0', + id: 0, + method: 'fileDelete', + params: { path: path }, + } as { + jsonrpc: string; + }); + + for (let i = 0; i < workspacePath.length; i++) { + if (path[i] !== workspacePath[i]) { + return; + } + } + + await input.onFileDelete(path.slice(workspacePath.length)); + }; + + const configParams = createDefaultConfigParams( + homeDir, + files, + defaultFile, + document.body, + onFileUpdate, + onFileDelete, + ); + + const onLoad = async (wrapper: MonacoEditorLanguageClientWrapper) => { + const result = wrapper.getExtensionRegisterResult( + 'mlc-python-example', + ) as RegisterLocalProcessExtensionResult; + await result.setAsDefaultApi(); + + const initResult = wrapper.getExtensionRegisterResult( + 'debugger-py-client', + ) as RegisterLocalProcessExtensionResult | undefined; + if (initResult !== undefined) { + configureDebugging(await initResult.getApi(), configParams); + } + + // Send all current files + await Promise.all( + configParams.fileSystemProvider.getAllFiles().map(async (file) => { + await writer.write({ + jsonrpc: '2.0', + id: 0, + method: 'fileSync', + params: { + path: file.path, + text: file.text, + updated: file.updated, + }, + } as { + jsonrpc: string; + }); + }), + ); + + await writer.write({ + jsonrpc: '2.0', + id: 0, + method: 'pipInstall', + params: { + path: workspacePath, + }, + } as { + jsonrpc: string; + }); + + await vscode.commands.executeCommand('workbench.view.explorer'); + if (configParams.defaultFile) { + await vscode.window.showTextDocument(configParams.defaultFile); + } + }; + const wrapperConfig: WrapperConfig = { $type: 'extended', htmlContainer: configParams.htmlContainer, @@ -110,33 +279,47 @@ export const createWrapperConfig = (): PythonAppConfig => { startOptions: { onCall: (languageClient?: MonacoLanguageClient) => { setTimeout(() => { - ['pyright.restartserver', 'pyright.organizeimports'].forEach((cmdName) => { - vscode.commands.registerCommand(cmdName, (...args: unknown[]) => { - languageClient?.sendRequest('workspace/executeCommand', { command: cmdName, arguments: args }); - }); + [ + 'pyright.restartserver', + 'pyright.organizeimports', + ].forEach((cmdName) => { + vscode.commands.registerCommand( + cmdName, + (...args: unknown[]) => { + void languageClient?.sendRequest( + 'workspace/executeCommand', + { + command: cmdName, + arguments: args, + }, + ); + }, + ); }); }, 250); }, reportStatus: true, - } + }, }, - messageTransports: { reader, writer } + messageTransports: { reader, writer }, }, clientOptions: { documentSelector: [configParams.languageId], workspaceFolder: { index: 0, name: configParams.workspaceRoot, - uri: vscode.Uri.parse(configParams.workspaceRoot) + uri: vscode.Uri.parse(configParams.workspaceRoot), }, - } - } + }, + }, }, vscodeApiConfig: { serviceOverrides: { ...getKeybindingsServiceOverride(), ...getLifecycleServiceOverride(), - ...getLocalizationServiceOverride(createDefaultLocaleConfiguration()), + ...getLocalizationServiceOverride( + createDefaultLocaleConfiguration(), + ), ...getBannerServiceOverride(), ...getStatusBarServiceOverride(), ...getTitleBarServiceOverride(), @@ -144,16 +327,22 @@ export const createWrapperConfig = (): PythonAppConfig => { ...getRemoteAgentServiceOverride(), ...getEnvironmentServiceOverride(), ...getSecretStorageServiceOverride(), - ...getStorageServiceOverride(), + ...getStorageServiceOverride({ + fallbackOverride: { + 'workbench.activity.showAccounts': false, + }, + }), ...getSearchServiceOverride(), ...getDebugServiceOverride(), ...getTestingServiceOverride(), - ...getPreferencesServiceOverride() + ...getPreferencesServiceOverride(), + ...getWorkingCopyServiceOverride(), }, viewsConfig: { viewServiceType: 'ViewsService', - htmlAugmentationInstructions: defaultHtmlAugmentationInstructions, - viewsInitFunc: defaultViewsInit + htmlAugmentationInstructions: + defaultHtmlAugmentationInstructions, + viewsInitFunc: defaultViewsInit, }, userConfiguration: { json: JSON.stringify({ @@ -161,15 +350,17 @@ export const createWrapperConfig = (): PythonAppConfig => { 'editor.guides.bracketPairsHorizontal': 'active', 'editor.wordBasedSuggestions': 'off', 'editor.experimental.asyncTokenization': true, - 'debug.toolBarLocation': 'docked' - }) + 'debug.toolBarLocation': 'docked', + 'files.autoSave': 'afterDelay', + 'files.autoSaveDelay': 100, + }), }, workspaceConfig: { enableWorkspaceTrust: true, windowIndicator: { label: 'mlc-python-example', tooltip: '', - command: '' + command: '', }, workspaceProvider: { trusted: true, @@ -178,16 +369,17 @@ export const createWrapperConfig = (): PythonAppConfig => { return true; }, workspace: { - workspaceUri: configParams.workspaceFile - } + workspaceUri: configParams.workspaceFile, + }, }, configurationDefaults: { - 'window.title': 'mlc-python-example${separator}${dirty}${activeEditorShort}' + 'window.title': + 'mlc-python-example${separator}${dirty}${activeEditorShort}', }, productConfiguration: { nameShort: 'mlc-python-example', - nameLong: 'mlc-python-example' - } + nameLong: 'mlc-python-example', + }, }, }, extensions: [ @@ -197,19 +389,20 @@ export const createWrapperConfig = (): PythonAppConfig => { publisher: 'TypeFox', version: '1.0.0', engines: { - vscode: '*' - } - } + vscode: '*', + }, + }, }, - provideDebuggerExtensionConfig(configParams) + provideDebuggerExtensionConfig(configParams), ], editorAppConfig: { - monacoWorkerFactory: configureDefaultWorkerFactory - } + monacoWorkerFactory: configureDefaultWorkerFactory, + }, }; return { wrapperConfig, - configParams: configParams + configParams: configParams, + onLoad, }; }; diff --git a/packages/examples/src/python/client/main.ts b/packages/examples/src/python/client/main.ts index b73f44609..9d883512e 100644 --- a/packages/examples/src/python/client/main.ts +++ b/packages/examples/src/python/client/main.ts @@ -7,10 +7,41 @@ import * as vscode from 'vscode'; import { type RegisterLocalProcessExtensionResult } from '@codingame/monaco-vscode-api/extensions'; import { MonacoEditorLanguageClientWrapper } from 'monaco-editor-wrapper'; import { createWrapperConfig } from './config.js'; -import { confiugureDebugging } from '../../debugger/client/debugger.js'; +import { configureDebugging } from '../../debugger/client/debugger.js'; +import requirementsCode from '../../../resources/python/requirements.txt?raw'; +import mainPyCode from '../../../resources/python/main.py?raw'; +import hello2PyCode from '../../../resources/python/hello2.py?raw'; +import badPyCode from '../../../resources/python/bad.py?raw'; +import type { Files } from '../../debugger/common/serverSyncingFileSystemProvider.js'; + +const files: Files = { + 'requirements.txt': { + updated: Date.now(), + text: requirementsCode, + }, + 'main.py': { + updated: Date.now(), + text: mainPyCode, + }, + 'hello2.py': { + updated: Date.now(), + text: hello2PyCode, + }, + 'bad.py': { updated: Date.now(), text: badPyCode }, +}; export const runPythonWrapper = async () => { - const appConfig = createWrapperConfig(); + const appConfig = createWrapperConfig({ + files, + onFileUpdate: (file) => { + console.error('[FILE] file updated', file); + return Promise.resolve(); + }, + onFileDelete: (path) => { + console.error('[FILE] file deleted', path); + return Promise.resolve(); + }, + }); const wrapper = new MonacoEditorLanguageClientWrapper(); if (wrapper.isStarted()) { @@ -18,17 +49,25 @@ export const runPythonWrapper = async () => { } else { await wrapper.init(appConfig.wrapperConfig); - const result = wrapper.getExtensionRegisterResult('mlc-python-example') as RegisterLocalProcessExtensionResult; + const result = wrapper.getExtensionRegisterResult( + 'mlc-python-example', + ) as RegisterLocalProcessExtensionResult; result.setAsDefaultApi(); - const initResult = wrapper.getExtensionRegisterResult('debugger-py-client') as RegisterLocalProcessExtensionResult | undefined; + const initResult = wrapper.getExtensionRegisterResult( + 'debugger-py-client', + ) as RegisterLocalProcessExtensionResult | undefined; if (initResult !== undefined) { - confiugureDebugging(await initResult.getApi(), appConfig.configParams); + configureDebugging( + await initResult.getApi(), + appConfig.configParams, + ); } await vscode.commands.executeCommand('workbench.view.explorer'); - await vscode.window.showTextDocument(appConfig.configParams.files.get('hello2.py')!.uri); await wrapper.start(); + + appConfig.onLoad(wrapper); } }; diff --git a/packages/examples/src/python/client/reactPython.tsx b/packages/examples/src/python/client/reactPython.tsx index ae280db3a..ac7a94c2f 100644 --- a/packages/examples/src/python/client/reactPython.tsx +++ b/packages/examples/src/python/client/reactPython.tsx @@ -1,45 +1,60 @@ /* -------------------------------------------------------------------------------------------- * Copyright (c) 2024 TypeFox and others. * Licensed under the MIT License. See LICENSE in the package root for license information. -* ------------------------------------------------------------------------------------------ */ + * ------------------------------------------------------------------------------------------ */ -import * as vscode from 'vscode'; -import { type RegisterLocalProcessExtensionResult } from '@codingame/monaco-vscode-api/extensions'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { MonacoEditorReactComp } from '@typefox/monaco-editor-react'; -import { MonacoEditorLanguageClientWrapper } from 'monaco-editor-wrapper'; -import { createWrapperConfig } from './config.js'; -import { confiugureDebugging } from '../../debugger/client/debugger.js'; +import { createWrapperConfig } from './config.js'; +import requirementsCode from '../../../resources/python/requirements.txt?raw'; +import mainPyCode from '../../../resources/python/main.py?raw'; +import hello2PyCode from '../../../resources/python/hello2.py?raw'; +import badPyCode from '../../../resources/python/bad.py?raw'; +import type { Files } from '../../debugger/common/serverSyncingFileSystemProvider.js'; -export const runPythonReact = async () => { - const appConfig = createWrapperConfig(); - - const onLoad = async (wrapper: MonacoEditorLanguageClientWrapper) => { - const result = wrapper.getExtensionRegisterResult('mlc-python-example') as RegisterLocalProcessExtensionResult; - result.setAsDefaultApi(); +const files: Files = { + 'requirements.txt': { + updated: Date.now(), + text: requirementsCode, + }, + 'main.py': { + updated: Date.now(), + text: mainPyCode, + }, + 'hello2.py': { + updated: Date.now(), + text: hello2PyCode, + }, + 'bad.py': { updated: Date.now(), text: badPyCode }, +}; - const initResult = wrapper.getExtensionRegisterResult('debugger-py-client') as RegisterLocalProcessExtensionResult | undefined; - if (initResult !== undefined) { - confiugureDebugging(await initResult.getApi(), appConfig.configParams); +export const runPythonReact = async () => { + const appConfig = createWrapperConfig({ + files, + onFileUpdate: (file) => { + console.error('[FILE] file updated', file); + return Promise.resolve(); + }, + onFileDelete: (path) => { + console.error('[FILE] file deleted', path); + return Promise.resolve(); } - - await vscode.commands.executeCommand('workbench.view.explorer'); - await vscode.window.showTextDocument(appConfig.configParams.files.get('hello2.py')!.uri); - }; + }); const root = ReactDOM.createRoot(document.getElementById('react-root')!); const App = () => { return ( -
+
{ console.error(e); - }} /> + }} + />
); }; diff --git a/packages/examples/src/python/server/main.ts b/packages/examples/src/python/server/main.ts index 1e736b5df..8bb1b967d 100644 --- a/packages/examples/src/python/server/main.ts +++ b/packages/examples/src/python/server/main.ts @@ -8,34 +8,40 @@ import { IncomingMessage } from 'node:http'; import { runLanguageServer } from '../../common/node/language-server-runner.js'; import { LanguageName } from '../../common/node/server-commons.js'; +const port = parseInt(process.env.PORT ?? '', 10) || 30001; + export const runPythonServer = (baseDir: string, relativeDir: string) => { const processRunPath = resolve(baseDir, relativeDir); runLanguageServer({ serverName: 'PYRIGHT', pathName: '/pyright', - serverPort: 30001, + serverPort: port, runCommand: LanguageName.node, - runCommandArgs: [ - processRunPath, - '--stdio' - ], + runCommandArgs: [processRunPath, '--stdio', '--verbose'], + spawnOptions: { shell: true }, wsServerOptions: { noServer: true, perMessageDeflate: false, clientTracking: true, verifyClient: ( - clientInfo: { origin: string; secure: boolean; req: IncomingMessage }, - callback + clientInfo: { + origin: string; + secure: boolean; + req: IncomingMessage; + }, + callback, ) => { - const parsedURL = new URL(`${clientInfo.origin}${clientInfo.req.url ?? ''}`); + const parsedURL = new URL( + `${clientInfo.origin}${clientInfo.req.url ?? ''}`, + ); const authToken = parsedURL.searchParams.get('authorization'); if (authToken === 'UserAuth') { callback(true); } else { callback(false); } - } + }, }, - logMessages: true + logMessages: true, }); }; diff --git a/packages/examples/style.css b/packages/examples/style.css index cc4063748..5fc4831e2 100644 --- a/packages/examples/style.css +++ b/packages/examples/style.css @@ -1,5 +1,15 @@ body { font-family: Helvetica, Arial, Sans-Serif; + width: 100%; + display: flex; + flex-direction: column; + min-height: 0; + height: 100vh; + overflow-x: hidden; + overscroll-behavior-x: none; + overflow-y: hidden; + overscroll-behavior-y: none; + -ms-overflow-style: scrollbar; } .exampleHeadelineDiv { @@ -10,3 +20,8 @@ body { font-weight: bold; font-size: 20px; } + +#react-root { + flex: 1; + min-height: 0; +} \ No newline at end of file diff --git a/packages/wrapper/src/vscode/viewsService.ts b/packages/wrapper/src/vscode/viewsService.ts index 3e372c9ef..371efce9e 100644 --- a/packages/wrapper/src/vscode/viewsService.ts +++ b/packages/wrapper/src/vscode/viewsService.ts @@ -4,48 +4,77 @@ * ------------------------------------------------------------------------------------------ */ export const defaultViewsInit = async () => { - const { Parts, Position, onPartVisibilityChange, isPartVisibile, attachPart, getSideBarPosition, onDidChangeSideBarPosition } = await import('@codingame/monaco-vscode-views-service-override'); + const { + Parts, + Position, + onPartVisibilityChange, + isPartVisibile, + attachPart, + getSideBarPosition, + onDidChangeSideBarPosition, + } = await import('@codingame/monaco-vscode-views-service-override'); for (const config of [ { part: Parts.TITLEBAR_PART, element: '#titleBar' }, { part: Parts.BANNER_PART, element: '#banner' }, { - part: Parts.SIDEBAR_PART, get element() { - return getSideBarPosition() === Position.LEFT ? '#sidebar' : '#sidebar-right'; - }, onDidElementChange: onDidChangeSideBarPosition + part: Parts.SIDEBAR_PART, + get element() { + return getSideBarPosition() === Position.LEFT + ? '#sidebar' + : '#sidebar-right'; + }, + onDidElementChange: onDidChangeSideBarPosition, }, { - part: Parts.ACTIVITYBAR_PART, get element() { - return getSideBarPosition() === Position.LEFT ? '#activityBar' : '#activityBar-right'; - }, onDidElementChange: onDidChangeSideBarPosition + part: Parts.ACTIVITYBAR_PART, + get element() { + return getSideBarPosition() === Position.LEFT + ? '#activityBar' + : '#activityBar-right'; + }, + onDidElementChange: onDidChangeSideBarPosition, }, { - part: Parts.AUXILIARYBAR_PART, get element() { - return getSideBarPosition() === Position.LEFT ? '#auxiliaryBar' : '#auxiliaryBar-left'; - }, onDidElementChange: onDidChangeSideBarPosition + part: Parts.AUXILIARYBAR_PART, + get element() { + return getSideBarPosition() === Position.LEFT + ? '#auxiliaryBar' + : '#auxiliaryBar-left'; + }, + onDidElementChange: onDidChangeSideBarPosition, }, { part: Parts.EDITOR_PART, element: '#editors' }, { part: Parts.PANEL_PART, element: '#panel' }, - { part: Parts.STATUSBAR_PART, element: '#statusBar' } + { part: Parts.STATUSBAR_PART, element: '#statusBar' }, ]) { - attachPart(config.part, document.querySelector(config.element)!); + attachPart( + config.part, + document.querySelector(config.element)!, + ); config.onDidElementChange?.(() => { - attachPart(config.part, document.querySelector(config.element)!); + attachPart( + config.part, + document.querySelector(config.element)!, + ); }); if (!isPartVisibile(config.part)) { - document.querySelector(config.element)!.style.display = 'none'; + document.querySelector( + config.element, + )!.style.display = 'none'; } - onPartVisibilityChange(config.part, visible => { - document.querySelector(config.element)!.style.display = visible ? 'block' : 'none'; + onPartVisibilityChange(config.part, (visible) => { + document.querySelector( + config.element, + )!.style.display = visible ? 'block' : 'none'; }); } }; -export const defaultViewsHtml = `
-
+export const defaultViewsHtml = `
@@ -63,11 +92,13 @@ export const defaultViewsHtml = `
-
-
`; +
`; -export const defaultHtmlAugmentationInstructions = (htmlElement: HTMLElement | null | undefined) => { +export const defaultHtmlAugmentationInstructions = ( + htmlElement: HTMLElement | null | undefined, +) => { const htmlContainer = document.createElement('div', { is: 'app' }); + htmlContainer.setAttribute('id', 'workbench-container'); htmlContainer.innerHTML = defaultViewsHtml; - htmlElement?.append(htmlContainer); + htmlElement?.replaceChildren(htmlContainer); };