Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-ui-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
node-version: "22.x"
cache: npm

- name: Install dependencies
Expand Down
1,213 changes: 650 additions & 563 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,44 @@
"src/ui",
"tests/e2e"
],
"engines": {
"node": "22.x.x",
"npm": "10.x.x"
},
"engines": {
"node": "22.x.x",
"npm": "10.x.x"
},
"scripts": {
"build": "npm run ui:build && npm run apps:build && npm run ui:codegen",
"test": "npm run --if-present -ws test",
"lint": "npm run --if-present -ws lint",
"dev": "npm run -w writer-ui dev",
"custom.dev": "npm run -w writer-ui custom.dev",

"cli:test": "pytest tests -o log_cli=true ",
"cli:lint": "mypy ./src/writer --exclude app_templates/* && ruff check",
"cli:build": "npm run ui:codegen",

"ui:codegen": "npm run -w writer-ui codegen",
"ui:codegen": "npm run -w writer-ui codegen",
"ui:dev": "npm run -w writer-ui dev",
"ui:build": "npm run -w writer-ui build",
"ui:preview": "npm run -w writer-ui preview",
"ui:custom.build": "npm run -w writer-ui custom.build",
"ui:custom.check": "npm run -w writer-ui custom.check",
"ui:lint": "npm run -w writer-ui lint",
"ui:lint.ci": "npm run -w writer-ui lint.ci",

"e2e": "npm run -w writer-e2e e2e",
"e2e:setup": "npm run -w writer-e2e e2e:setup",
"e2e:ui": "npm run -w writer-e2e e2e:ui",
"e2e:ci": "npm run -w writer-e2e e2e:ci",
"e2e:firefox": "npm run -w writer-e2e e2e:firefox",
"e2e:chromium": "npm run -w writer-e2e e2e:chromium",
"e2e:webkit": "npm run -w writer-e2e e2e:webkit",

"apps:build": "cp -R ./apps/hello ./src/writer/app_templates/ && cp -R ./apps/default ./src/writer/app_templates/",
"codegen": "npm run ui:codegen",
"ld:upload-sourcemaps": "./scripts/upload-sourcemaps.sh",
"build:with-sourcemaps": "npm run ui:build && npm run ld:upload-sourcemaps"
},
"devDependencies": {
"playwright": "^1.57.0"
},
"dependencies": {
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^24.2.0",
"vscode": "npm:@codingame/monaco-vscode-extension-api@^24.2.0"
}
}
765 changes: 760 additions & 5 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ writer-sdk = ">= 2.3.1, < 3"
python-multipart = ">=0.0.7, < 1"
orjson = "^3.11.0, <4"
launchdarkly-server-sdk = "^9.12.0"
python-lsp-server = {extras = ["all"], version = "^1.12.0"}

[tool.poetry.group.build]
optional = true
Expand Down
16 changes: 12 additions & 4 deletions src/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "NODE_OPTIONS='--max-old-space-size=8192' vite build",
"preview": "vite preview --port 5050",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path ../../.gitignore --ignore-path .gitignore",
"lint.ci": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path ../../.gitignore --ignore-path .gitignore",
Expand All @@ -18,10 +18,15 @@
"dependencies": {
"@apache-arrow/ts": "^15.0.2",
"@chamaeleonidae/chmln": "^1.0.1",
"@codingame/monaco-vscode-api": "^24.2.0",
"@codingame/monaco-vscode-languages-service-override": "^24.2.0",
"@codingame/monaco-vscode-textmate-service-override": "^24.2.0",
"@floating-ui/vue": "^1.1.5",
"@fontsource/poppins": "^5.0.14",
"@fullstory/browser": "^2.0.6",
"@googlemaps/js-api-loader": "^1.16.6",
"@launchdarkly/observability": "^0.4.0",
"@launchdarkly/session-replay": "^0.4.5",
"@monaco-editor/loader": "^1.3.3",
"@tato30/vue-pdf": "^1.11.3",
"@tiptap/extension-document": "^3.0.7",
Expand All @@ -36,19 +41,22 @@
"arquero": "^5.2.0",
"attr-accept": "^2.2.5",
"chroma-js": "^2.4.2",
"@launchdarkly/observability": "^0.4.0",
"@launchdarkly/session-replay": "^0.4.5",
"fast-uri": "^3.1.0",
"launchdarkly-js-client-sdk": "^3.8.1",
"lucide": "^0.525.0",
"mapbox-gl": "^3.2.0",
"marked": "^12.0.1",
"monaco-editor": "^0.47.0",
"monaco-editor": "^0.52.0",
"monaco-languageclient": "^10.5.0",
"monacopilot": "^1.2.9",
"plotly.js-dist-min": "^2.35.2",
"pretty-bytes": "^6.1.1",
"typescript": "^5.4.3",
"vega": "^5.22.1",
"vega-embed": "^6.22.1",
"vega-lite": "^5.7.1",
"vscode-languageclient": "^9.0.1",
"vscode-ws-jsonrpc": "^3.5.0",
"vue": "^3.5.0",
"vue-dompurify-html": "^5.0.1"
},
Expand Down
188 changes: 168 additions & 20 deletions src/ui/src/builder/BuilderEmbeddedCodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
ref="rootEl"
class="BuilderEmbeddedCodeEditor"
:class="{
'BuilderEmbeddedCodeEditor--full': variant === 'full',
'BuilderEmbeddedCodeEditor--halfScreen': variant === 'half-screen',
'BuilderEmbeddedCodeEditor--singleLine': variant === 'single-line',
'BuilderEmbeddedCodeEditor--full': props.variant === 'full',
'BuilderEmbeddedCodeEditor--halfScreen':
props.variant === 'half-screen',
'BuilderEmbeddedCodeEditor--singleLine':
props.variant === 'single-line',
}"
>
<div ref="editorContainerEl" class="editorContainer"></div>
Expand All @@ -18,16 +20,26 @@ import "./builderEditorWorker";
import {
onMounted,
onUnmounted,
PropType,
type PropType,
toRefs,
useTemplateRef,
watch,
} from "vue";
import { syncModelWithLSP } from "./lsp/lspModelSync";
import { setModelDiagnostics, setupLSPDiagnostics } from "./lsp/lspDiagnostics";
import { useMonacopilot } from "../composables/useMonacopilot";
import { useLogger } from "@/composables/useLogger";
import { useCodeEditorSettings } from "@/composables/useCodeEditorSettings";

const rootEl = useTemplateRef("rootEl");
const editorContainerEl = useTemplateRef("editorContainerEl");
const resizeObserver = new ResizeObserver(updateDimensions);
let editor: monaco.editor.IStandaloneCodeEditor = null;
let lspSyncDisposable: monaco.IDisposable | null = null;
let diagnosticsDisposable: monaco.IDisposable | null = null;
let monacopilotCleanup: (() => void) | null = null;

const { diagnosticsEnabled, aiCompletionEnabled } = useCodeEditorSettings();

type EditorVariant = "full" | "minimal" | "half-screen" | "single-line";

Expand All @@ -44,6 +56,8 @@ const props = defineProps({
const { modelValue, disabled, language } = toRefs(props);
const emit = defineEmits(["update:modelValue"]);

const logger = useLogger();

const VARIANTS_SETTINGS: Partial<
Record<
EditorVariant,
Expand All @@ -54,6 +68,7 @@ const VARIANTS_SETTINGS: Partial<
minimap: {
enabled: false,
},
tabCompletion: "on",
},
minimal: {
minimap: {
Expand Down Expand Up @@ -94,38 +109,146 @@ watch(disabled, (isNewDisabled) => {
});

watch(modelValue, (newCode) => {
if (editor.getValue() == newCode) return;
if (!editor || editor.getValue() === newCode) return;
editor.getModel().setValue(newCode);
});

watch(language, () => {
monaco.editor.setModelLanguage(editor.getModel(), language.value);
watch(language, (newLang) => {
if (!editor) return;
const model = editor.getModel();
if (model.getLanguageId() === newLang) return;

// Dispose old LSP sync before changing language
if (lspSyncDisposable) {
lspSyncDisposable.dispose();
lspSyncDisposable = null;
}

// Change language
monaco.editor.setModelLanguage(model, newLang);

// Re-sync if new language is Python
if (newLang === "python") {
try {
lspSyncDisposable = syncModelWithLSP(model);
} catch (error) {
logger.error("Failed to re-sync model with LSP:", error);
}
}
});

watch(diagnosticsEnabled, (enabled) => {
if (
!editor ||
props.language !== "python" ||
props.variant === "single-line"
) {
return;
}

const model = editor.getModel();
if (model && language.value === "python") {
diagnosticsDisposable?.dispose();
if (enabled) {
diagnosticsDisposable = setupLSPDiagnostics(monaco);
}
model.setValue(model.getValue());
}
});

// Watch AI completion setting changes
watch(aiCompletionEnabled, (enabled) => {
if (
!editor ||
props.language !== "python" ||
props.variant === "single-line"
)
return;

if (enabled) {
// Enable AI completion
if (!monacopilotCleanup) {
try {
monacopilotCleanup = useMonacopilot(
monaco,
editor,
props.language,
);
} catch (error) {
logger.error("Failed to enable AI completion:", error);
}
}
} else {
// Disable AI completion
if (monacopilotCleanup) {
monacopilotCleanup();
monacopilotCleanup = null;
}
}
});

onMounted(() => {
editor = monaco.editor.create(editorContainerEl.value, {
value: modelValue.value ?? "",
language: props.language,
onMounted(async () => {
// Create model with proper URI for LSP
const modelUri = monaco.Uri.parse(`inmemory://model/${Date.now()}.py`);
const model = monaco.editor.createModel(
modelValue.value ?? "",
props.language || "python",
props.language === "python" ? modelUri : undefined,
);

editor = monaco.editor.create(editorContainerEl.value as HTMLElement, {
model: model,
readOnly: props.disabled,
fixedOverflowWidgets: true,
quickSuggestions: {
other: true,
comments: true,
strings: true,
},
...VARIANTS_SETTINGS[props.variant],
});
editor.getModel().onDidChangeContent(() => {

model.onDidChangeContent(() => {
const newCode = editor.getValue();
emit("update:modelValue", newCode);
});
resizeObserver.observe(rootEl.value);

resizeObserver.observe(rootEl.value as Element);

// Manually sync model with LSP for Python language
// This is required because we're in a browser (no filesystem)
if (props.language === "python" && props.variant !== "single-line") {
try {
lspSyncDisposable = syncModelWithLSP(model);
} catch (error) {
logger.error("Failed to sync model with LSP:", error);
}

// Register AI-powered code completions (if AI completion enabled)
if (aiCompletionEnabled.value) {
try {
monacopilotCleanup = useMonacopilot(
monaco,
editor,
props.language,
);
} catch (error) {
logger.error("Failed to initialize monacopilot:", error);
}
}

diagnosticsDisposable?.dispose();
if (diagnosticsEnabled.value) {
diagnosticsDisposable = setupLSPDiagnostics(monaco);
}
}

// when in modal, focus the editor and set the cursor to the last line
if (props.variant === "half-screen") {
editor.focus();
editor.setPosition({
lineNumber: editor.getModel().getLineCount(),
column: editor
.getModel()
.getLineLastNonWhitespaceColumn(
editor.getModel().getLineCount(),
),
lineNumber: model.getLineCount(),
column: model.getLineLastNonWhitespaceColumn(model.getLineCount()),
});
}
});
Expand All @@ -135,7 +258,32 @@ function updateDimensions() {
}

onUnmounted(() => {
editor.dispose();
// Clean up LSP sync
if (lspSyncDisposable) {
lspSyncDisposable.dispose();
lspSyncDisposable = null;
}

const model = editor?.getModel();

// Clear diagnostics before disposing model
if (model && language.value === "python") {
setModelDiagnostics(monaco, model, []);
}

if (editor) {
editor.dispose();
}
if (model) {
model.dispose();
}
if (monacopilotCleanup) {
monacopilotCleanup();
}
if (diagnosticsDisposable) {
diagnosticsDisposable.dispose();
diagnosticsDisposable = null;
}
resizeObserver.disconnect();
});
</script>
Expand Down
Loading