From 5153924bbac7a02f01b5c8e2eff19f4698e0ba11 Mon Sep 17 00:00:00 2001 From: lucebac Date: Sat, 21 Sep 2024 03:39:47 +0200 Subject: [PATCH 1/2] FEAT: add code navigation to script editor Adds code navigation by registering a CodeEditorOpener to monaco. --- src/ScriptEditor/ScriptEditor.ts | 3 + src/ScriptEditor/ui/ScriptEditorRoot.tsx | 104 +++++++++++++++++++++-- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/src/ScriptEditor/ScriptEditor.ts b/src/ScriptEditor/ScriptEditor.ts index ba031f74a..168f00726 100644 --- a/src/ScriptEditor/ScriptEditor.ts +++ b/src/ScriptEditor/ScriptEditor.ts @@ -14,6 +14,7 @@ import { NetscriptExtra } from "../NetscriptFunctions/Extra"; import * as enums from "../Enums"; import { ns } from "../NetscriptFunctions"; import { isLegacyScript } from "../Paths/ScriptFilePath"; +import { CodeOpener } from "./ui/ScriptEditorRoot"; /** Event emitter used for tracking when changes have been made to a content file. */ export const fileEditEvents = new EventEmitter<[hostname: string, filename: ContentFilePath]>(); @@ -126,6 +127,8 @@ export class ScriptEditor { }); } + monaco.editor.registerEditorOpener(new CodeOpener()); + monaco.languages.json.jsonDefaults.setModeConfiguration({ ...monaco.languages.json.jsonDefaults.modeConfiguration, //completion should be disabled because the diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index 9390c472a..9b2e8bea8 100644 --- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx +++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx @@ -32,7 +32,7 @@ import { useVimEditor } from "./useVimEditor"; import { useCallback } from "react"; import { type AST, getFileType, parseAST } from "../../utils/ScriptTransformer"; import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes"; -import { hasScriptExtension, isLegacyScript } from "../../Paths/ScriptFilePath"; +import { hasScriptExtension, isLegacyScript, resolveScriptFilePath, ScriptFilePath } from "../../Paths/ScriptFilePath"; interface IProps { // Map of filename -> code @@ -42,6 +42,74 @@ interface IProps { } const openScripts: OpenScript[] = []; let currentScript: OpenScript | null = null; +let vim: boolean = false; // this will be set on mounting the component + +export class CodeOpener implements monaco.editor.ICodeEditorOpener { + openCodeEditor( + source: monaco.editor.ICodeEditor, + resource: monaco.Uri, + selectionOrPosition?: monaco.IRange | monaco.IPosition, + ): boolean { + const resolvedPath = resolveScriptFilePath(resource.path); + if (resolvedPath === null) { + return false; + } + + const [hostname, path] = resolvedPath.split("/", 2); + + // check if we have an open file already; if so, show it at the requested position + const openScript = openScripts.find((s) => s.path === path && s.hostname === hostname); + if (openScript) { + if (openScript.model === undefined || openScript.model === null || openScript.model.isDisposed()) { + openScript.regenerateModel(); + } + currentScript = openScript; + source.setModel(openScript.model); + } else { + const server = GetServer(hostname); + if (server === null) { + return false; + } + + const script = server.scripts.get(path as ScriptFilePath); + if (script === undefined) { + return false; + } + + // Open script + const newScript = new OpenScript( + path as ScriptFilePath, + script.code, + hostname, + new monaco.Position(0, 0), + makeModel(hostname, path, script.code), + vim, + ); + openScripts.push(newScript); + currentScript = newScript; + source.setModel(newScript.model); + } + + if (selectionOrPosition === undefined) { + return true; + } + + if ((selectionOrPosition as monaco.IRange) !== undefined) { + const range = selectionOrPosition as monaco.IRange; + + source.setSelection(range); + source.revealLineInCenter(range.startLineNumber); + } else if ((selectionOrPosition as monaco.IPosition) !== undefined) { + const pos = selectionOrPosition as monaco.IPosition; + + source.setPosition(pos); + source.revealLineInCenter(pos.lineNumber); + } + + source.focus(); + return true; + } +} function Root(props: IProps): React.ReactElement { const rerender = useRerender(); @@ -187,11 +255,33 @@ function Root(props: IProps): React.ReactElement { debouncedCodeParsing(newCode); }; + let changeModelCallback: monaco.IDisposable | null = null; + // When the editor is mounted function onMount(editor: IStandaloneCodeEditor): void { // Required when switching between site navigation (e.g. from Script Editor -> Terminal and back) // the `useEffect()` for vim mode is called before editor is mounted. editorRef.current = editor; + changeModelCallback = editorRef.current.onDidChangeModel((e) => { + if (e.newModelUrl === null) { + return; + } + + const resolvedPath = resolveScriptFilePath(e.newModelUrl.path); + if (resolvedPath === null) { + return false; + } + const [hostname, path] = resolvedPath.split("/", 2); + + const script = openScripts.find((s) => s.hostname === hostname && s.path === path); + if (script) { + parseCode(script.code); + } + + rerender(); + removeOutlineOfEditor(); + }); + vim = props.vim; if (props.files.size === 0 && currentScript !== null) { // Open currentscript @@ -199,7 +289,6 @@ function Root(props: IProps): React.ReactElement { editorRef.current.setModel(currentScript.model); editorRef.current.setPosition(currentScript.lastPosition); editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber); - parseCode(currentScript.code); editorRef.current.focus(); return; } @@ -218,7 +307,6 @@ function Root(props: IProps): React.ReactElement { editorRef.current.setModel(openScript.model); editorRef.current.setPosition(openScript.lastPosition); editorRef.current.revealLineInCenter(openScript.lastPosition.lineNumber); - parseCode(openScript.code); } else { // Open script const newScript = new OpenScript( @@ -232,7 +320,6 @@ function Root(props: IProps): React.ReactElement { openScripts.push(newScript); currentScript = newScript; editorRef.current.setModel(newScript.model); - parseCode(newScript.code); } } @@ -289,7 +376,6 @@ function Root(props: IProps): React.ReactElement { editorRef.current.setModel(currentScript.model); editorRef.current.setPosition(currentScript.lastPosition); editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber); - parseCode(currentScript.code); editorRef.current.focus(); } removeOutlineOfEditor(); @@ -334,7 +420,6 @@ function Root(props: IProps): React.ReactElement { editorRef.current.setModel(currentScript.model); editorRef.current.setPosition(currentScript.lastPosition); editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber); - parseCode(currentScript.code); editorRef.current.focus(); } } @@ -366,9 +451,7 @@ function Root(props: IProps): React.ReactElement { openScript.regenerateModel(); } editorRef.current.setModel(openScript.model); - editorRef.current.setValue(openScript.code); - parseCode(openScript.code); editorRef.current.focus(); } } @@ -399,6 +482,11 @@ function Root(props: IProps): React.ReactElement { } function onUnmountEditor() { + if (changeModelCallback) { + changeModelCallback.dispose(); + changeModelCallback = null; + } + if (!currentScript) { return; } From 6dbad684a9c209b7982c09642a8c94d83d47d142 Mon Sep 17 00:00:00 2001 From: lucebac Date: Sat, 28 Sep 2024 20:07:53 +0200 Subject: [PATCH 2/2] use function instead of useless class wrapper --- src/ScriptEditor/ScriptEditor.ts | 4 +- src/ScriptEditor/ui/ScriptEditorRoot.tsx | 108 +++++++++++------------ 2 files changed, 55 insertions(+), 57 deletions(-) diff --git a/src/ScriptEditor/ScriptEditor.ts b/src/ScriptEditor/ScriptEditor.ts index 168f00726..fbdabc3a3 100644 --- a/src/ScriptEditor/ScriptEditor.ts +++ b/src/ScriptEditor/ScriptEditor.ts @@ -14,7 +14,7 @@ import { NetscriptExtra } from "../NetscriptFunctions/Extra"; import * as enums from "../Enums"; import { ns } from "../NetscriptFunctions"; import { isLegacyScript } from "../Paths/ScriptFilePath"; -import { CodeOpener } from "./ui/ScriptEditorRoot"; +import { openCodeEditor } from "./ui/ScriptEditorRoot"; /** Event emitter used for tracking when changes have been made to a content file. */ export const fileEditEvents = new EventEmitter<[hostname: string, filename: ContentFilePath]>(); @@ -127,7 +127,7 @@ export class ScriptEditor { }); } - monaco.editor.registerEditorOpener(new CodeOpener()); + monaco.editor.registerEditorOpener({openCodeEditor}); monaco.languages.json.jsonDefaults.setModeConfiguration({ ...monaco.languages.json.jsonDefaults.modeConfiguration, diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index 9b2e8bea8..c1c2275fc 100644 --- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx +++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx @@ -44,71 +44,69 @@ const openScripts: OpenScript[] = []; let currentScript: OpenScript | null = null; let vim: boolean = false; // this will be set on mounting the component -export class CodeOpener implements monaco.editor.ICodeEditorOpener { - openCodeEditor( - source: monaco.editor.ICodeEditor, - resource: monaco.Uri, - selectionOrPosition?: monaco.IRange | monaco.IPosition, - ): boolean { - const resolvedPath = resolveScriptFilePath(resource.path); - if (resolvedPath === null) { - return false; - } - - const [hostname, path] = resolvedPath.split("/", 2); - - // check if we have an open file already; if so, show it at the requested position - const openScript = openScripts.find((s) => s.path === path && s.hostname === hostname); - if (openScript) { - if (openScript.model === undefined || openScript.model === null || openScript.model.isDisposed()) { - openScript.regenerateModel(); - } - currentScript = openScript; - source.setModel(openScript.model); - } else { - const server = GetServer(hostname); - if (server === null) { - return false; - } +export function openCodeEditor( + source: monaco.editor.ICodeEditor, + resource: monaco.Uri, + selectionOrPosition?: monaco.IRange | monaco.IPosition, +): boolean { + const resolvedPath = resolveScriptFilePath(resource.path); + if (resolvedPath === null) { + return false; + } - const script = server.scripts.get(path as ScriptFilePath); - if (script === undefined) { - return false; - } + const [hostname, path] = resolvedPath.split("/", 2); - // Open script - const newScript = new OpenScript( - path as ScriptFilePath, - script.code, - hostname, - new monaco.Position(0, 0), - makeModel(hostname, path, script.code), - vim, - ); - openScripts.push(newScript); - currentScript = newScript; - source.setModel(newScript.model); + // check if we have an open file already; if so, show it at the requested position + const openScript = openScripts.find((s) => s.path === path && s.hostname === hostname); + if (openScript) { + if (openScript.model === undefined || openScript.model === null || openScript.model.isDisposed()) { + openScript.regenerateModel(); + } + currentScript = openScript; + source.setModel(openScript.model); + } else { + const server = GetServer(hostname); + if (server === null) { + return false; } - if (selectionOrPosition === undefined) { - return true; + const script = server.scripts.get(path as ScriptFilePath); + if (script === undefined) { + return false; } - if ((selectionOrPosition as monaco.IRange) !== undefined) { - const range = selectionOrPosition as monaco.IRange; + // Open script + const newScript = new OpenScript( + path as ScriptFilePath, + script.code, + hostname, + new monaco.Position(0, 0), + makeModel(hostname, path, script.code), + vim, + ); + openScripts.push(newScript); + currentScript = newScript; + source.setModel(newScript.model); + } - source.setSelection(range); - source.revealLineInCenter(range.startLineNumber); - } else if ((selectionOrPosition as monaco.IPosition) !== undefined) { - const pos = selectionOrPosition as monaco.IPosition; + if (selectionOrPosition === undefined) { + return true; + } - source.setPosition(pos); - source.revealLineInCenter(pos.lineNumber); - } + if ((selectionOrPosition as monaco.IRange) !== undefined) { + const range = selectionOrPosition as monaco.IRange; - source.focus(); - return true; + source.setSelection(range); + source.revealLineInCenter(range.startLineNumber); + } else if ((selectionOrPosition as monaco.IPosition) !== undefined) { + const pos = selectionOrPosition as monaco.IPosition; + + source.setPosition(pos); + source.revealLineInCenter(pos.lineNumber); } + + source.focus(); + return true; } function Root(props: IProps): React.ReactElement {