@finos/legend-application-pure-ide
Version:
Legend Pure IDE application core
370 lines (360 loc) • 13.7 kB
text/typescript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DISPLAY_ANSI_ESCAPE } from '@finos/legend-application';
import { assertErrorThrown } from '@finos/legend-shared';
import { flowResult } from 'mobx';
import { deserialize } from 'serializr';
import {
ConceptNode,
PackageConceptAttribute,
} from '../server/models/ConceptTree.js';
import { DirectoryNode } from '../server/models/DirectoryTree.js';
import { FileCoordinate } from '../server/models/File.js';
import {
HOME_DIRECTORY_PATH,
ROOT_PACKAGE_PATH,
WELCOME_FILE_PATH,
} from './PureIDEConfig.js';
import type { PureIDEStore } from './PureIDEStore.js';
import { LEGEND_PURE_IDE_TERMINAL_COMMAND } from '../__lib__/LegendPureIDECommand.js';
const PACKAGE_PATH_PATTERN = /^(?:(?:\w[\w$]*)::)*\w[\w$]*$/;
const FILE_PATH_PATTERN = /^\/?(?:\w+\/)*\w+(?:\.\w+)*$/;
const LEGEND_PURE_IDE_TERMINAL_WEBLINK_REGEX =
/(?:(?<url>https?:[/]{2}[^\s"'!*(){}|\\^<>`]*[^\s"':,.!?{}|\\^~[\]`()<>])|(?<path>resource:(?<path_sourceId>\/?(?:\w+\/)*\w+(?:\.\w+)*) (?:line:(?<path_line>\d+)) (?:column:(?<path_column>\d+))))/;
export const setupTerminal = (ideStore: PureIDEStore): void => {
ideStore.applicationStore.terminalService.terminal.setup({
webLinkProvider: {
handler: (event, text) => {
const match = text.match(LEGEND_PURE_IDE_TERMINAL_WEBLINK_REGEX);
if (match?.groups?.url) {
ideStore.applicationStore.navigationService.navigator.visitAddress(
match.groups.url,
);
} else if (
match?.groups?.path &&
match.groups.path_sourceId &&
match.groups.path_column &&
match.groups.path_line
) {
flowResult(
ideStore.loadFile(
match.groups.path_sourceId,
new FileCoordinate(
match.groups.path_sourceId,
Number.parseInt(match.groups.path_line, 10),
Number.parseInt(match.groups.path_column, 10),
),
),
).catch(ideStore.applicationStore.alertUnhandledError);
}
},
regex: LEGEND_PURE_IDE_TERMINAL_WEBLINK_REGEX,
},
// TODO: for these, we can potentially use `runCommand`, but we need to refactor
// command to return promises
// that is the cleaner way to do this and make us able to move terminal plugins/extension mechanism
// to LegendApplicationPlugin for example, rather than being application specific like this
// as for example, this requires access to `EditorStore` right now
commands: [
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.GO,
description: 'Run the go() function in welcome file',
usage: 'go',
aliases: ['compile', 'executeGo'],
handler: async (args: string[]): Promise<void> =>
flowResult(ideStore.executeGo()).catch(
ideStore.applicationStore.alertUnhandledError,
),
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.TEST,
description: 'Run the test suite (by path if specified)',
usage: 'test [/some/path]',
handler: async (args: string[]): Promise<void> => {
const path = args[0];
if (path) {
if (!path.match(PACKAGE_PATH_PATTERN)) {
ideStore.applicationStore.terminalService.terminal.fail(
`command requires a valid package/concept path`,
);
return;
}
}
await flowResult(
ideStore.executeTests(path ?? ROOT_PACKAGE_PATH),
).catch(ideStore.applicationStore.alertUnhandledError);
},
},
// io
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.REMOVE,
description: 'Remove a file or directory',
usage: 'rm /some/path',
handler: async (args: string[]): Promise<void> => {
const path = args[0];
if (!path?.match(FILE_PATH_PATTERN)) {
ideStore.applicationStore.terminalService.terminal.fail(
`rm command requires a valid file/directory path`,
);
return;
}
await flowResult(
ideStore.deleteDirectoryOrFile(path, undefined, undefined),
).catch(ideStore.applicationStore.alertUnhandledError);
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.MOVE,
description: 'Move a file',
usage: 'mv /old/path /new/path',
handler: async (args: string[]): Promise<void> => {
const oldPath = args[0];
if (!oldPath?.match(FILE_PATH_PATTERN)) {
ideStore.applicationStore.terminalService.terminal.fail(
`command requires a valid old file path`,
);
return;
}
const newPath = args[1];
if (!newPath?.match(FILE_PATH_PATTERN)) {
ideStore.applicationStore.terminalService.terminal.fail(
`command requires a valid new file path`,
);
return;
}
await flowResult(ideStore.renameFile(oldPath, newPath)).catch(
ideStore.applicationStore.alertUnhandledError,
);
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.NEW_DIRECTORY,
description: 'Create a new directory',
usage: 'mkdir /some/path',
handler: async (args: string[]): Promise<void> => {
const path = args[0];
if (!path?.match(FILE_PATH_PATTERN)) {
ideStore.applicationStore.terminalService.terminal.fail(
`command requires a valid directory path`,
);
return;
}
await flowResult(ideStore.createNewDirectory(path)).catch(
ideStore.applicationStore.alertUnhandledError,
);
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.NEW_FILE,
description: 'Create a new file',
usage: 'touch /some/path',
handler: async (args: string[]): Promise<void> => {
const path = args[0];
if (!path?.match(FILE_PATH_PATTERN)) {
ideStore.applicationStore.terminalService.terminal.fail(
`command requires a valid path`,
);
return;
}
await flowResult(ideStore.createNewDirectory(path)).catch(
ideStore.applicationStore.alertUnhandledError,
);
},
},
// navigation
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.WELCOME,
description: 'Open the welcome file',
usage: 'welcome',
aliases: ['start'],
handler: async (): Promise<void> => {
await flowResult(ideStore.loadFile(WELCOME_FILE_PATH)).catch(
ideStore.applicationStore.alertUnhandledError,
);
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.OPEN_FILE,
description: 'Open a file',
usage: 'open /some/file/path',
aliases: ['edit', 'code', 'vi'],
handler: async (args: string[]): Promise<void> => {
const path = args[0];
if (!path?.match(PACKAGE_PATH_PATTERN)) {
ideStore.applicationStore.terminalService.terminal.fail(
`command requires a valid file path`,
);
return;
}
await flowResult(ideStore.loadFile(path)).catch(
ideStore.applicationStore.alertUnhandledError,
);
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.OPEN_DIRECTORY,
description: 'Open a directory or a package',
usage: 'cd /some/directory/path | cd some::package::path',
handler: async (args: string[]): Promise<void> => {
const path = args[0];
if (
!path ||
!(path.match(FILE_PATH_PATTERN) ?? path.match(PACKAGE_PATH_PATTERN))
) {
ideStore.applicationStore.terminalService.terminal.fail(
`command requires a valid directory or concept path`,
);
return;
}
try {
// NOTE: favor concept/package path over directory path
if (path.match(PACKAGE_PATH_PATTERN)) {
await flowResult(
ideStore.conceptTreeState.revealConcept(path, {
forceOpenExplorerPanel: true,
packageOnly: true,
}),
);
} else {
await flowResult(
ideStore.directoryTreeState.revealPath(path, {
forceOpenExplorerPanel: true,
directoryOnly: true,
}),
);
}
} catch (error) {
assertErrorThrown(error);
ideStore.applicationStore.terminalService.terminal.fail(
error.message,
);
}
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.LIST_DIRECTORY,
description: 'List children of a directory or package',
usage: 'cd /some/directory/path | cd some::package::path | cd ::',
handler: async (args: string[]): Promise<void> => {
const path = args[0];
if (
!path ||
!(
path.match(FILE_PATH_PATTERN) ??
path.match(PACKAGE_PATH_PATTERN) ??
[HOME_DIRECTORY_PATH, ROOT_PACKAGE_PATH].includes(path)
)
) {
ideStore.applicationStore.terminalService.terminal.fail(
`command requires a valid directory or package path`,
);
return;
}
try {
// NOTE: favor concept/package path over directory path
if (
path.match(PACKAGE_PATH_PATTERN) ||
path === ROOT_PACKAGE_PATH
) {
ideStore.applicationStore.terminalService.terminal.output(
(await ideStore.client.getConceptChildren(path))
.map((child) => deserialize(ConceptNode, child))
.map((child) =>
child.li_attr instanceof PackageConceptAttribute
? `${DISPLAY_ANSI_ESCAPE.BRIGHT_CYAN}${child.text}${DISPLAY_ANSI_ESCAPE.RESET}`
: child.text,
)
.join('\n'),
);
} else {
ideStore.applicationStore.terminalService.terminal.output(
(await ideStore.client.getDirectoryChildren(path))
.map((child) => deserialize(DirectoryNode, child))
.map((child) =>
child.isFolderNode
? `${DISPLAY_ANSI_ESCAPE.BRIGHT_CYAN}${child.text}${DISPLAY_ANSI_ESCAPE.RESET}`
: child.text,
)
.join('\n'),
);
}
} catch (error) {
assertErrorThrown(error);
ideStore.applicationStore.terminalService.terminal.fail(
error.message,
);
}
},
},
// utility
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.CLEAR,
description: 'Clear the terminal',
usage: 'clear',
handler: async (args: string[]): Promise<void> => {
ideStore.applicationStore.terminalService.terminal.clear();
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.ECHO,
description: 'Print text',
usage: `echo 'some string'`,
handler: async (
args: string[],
command: string,
text: string,
): Promise<void> => {
const content = text
.substring(text.indexOf(command) + command.length)
.trim();
ideStore.applicationStore.terminalService.terminal.output(
content.replaceAll(/\\u001b/g, '\u001b'),
);
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.ANSI,
description: 'Show common ANSI escape sequences used for styling',
usage: 'ansi',
handler: async (args: string[]): Promise<void> => {
ideStore.applicationStore.terminalService.terminal.showCommonANSIEscapeSequences();
return Promise.resolve();
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.DEBUG,
description:
'Introspect debug state. When passing no parameters, will display summary of available variables',
usage: 'debug [summary | abort | <expression to evaluate> ]',
aliases: [],
handler: async (args: string[]): Promise<void> => {
flowResult(ideStore.runDebugger({ args })).catch(
ideStore.applicationStore.alertUnhandledError,
);
},
},
{
command: LEGEND_PURE_IDE_TERMINAL_COMMAND.HELP,
description: 'Show help',
usage: 'help',
handler: async (args: string[]): Promise<void> => {
ideStore.applicationStore.terminalService.terminal.showHelp();
return Promise.resolve();
},
},
],
});
};