From 06f8255df99c1f5f7daf52987a7c8fe258622153 Mon Sep 17 00:00:00 2001
From: erdfern <rexsomnia@pm.me>
Date: Thu, 13 Mar 2025 16:19:21 +0100
Subject: [PATCH] --wip--

---
 bun.lock                                      |  11 +
 db/schema/schema.surql                        |  16 +
 devenv.lock                                   |  49 ++-
 .../CodeBlock/AnnotationEditor.svelte         | 147 +++++++
 .../CodeBlock/AnnotationList.svelte           | 137 +++++++
 src/lib/components/CodeBlock/CodeBlock.svelte | 369 ++++++++++++++++--
 src/lib/components/CodeBlock/transformers.ts  |  65 ++-
 src/lib/components/CodeBlock/types.ts         |  67 +++-
 src/routes/(app)/debug/editor/+page.svelte    | 140 ++++++-
 9 files changed, 920 insertions(+), 81 deletions(-)
 create mode 100644 src/lib/components/CodeBlock/AnnotationEditor.svelte
 create mode 100644 src/lib/components/CodeBlock/AnnotationList.svelte

diff --git a/bun.lock b/bun.lock
index 72be1ea8..95ec505b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -10,6 +10,7 @@
         "highlight.js": "^11.11.1",
         "js-cookie": "^3.0.5",
         "lucide-svelte": "^0.479.0",
+        "monaco-editor": "^0.52.2",
         "pino": "^9.6.0",
         "pino-pretty": "^13.0.0",
         "surrealdb": "^1.2.1",
@@ -19,8 +20,10 @@
       },
       "devDependencies": {
         "@iconify/svelte": "^4.2.0",
+        "@monaco-editor/loader": "^1.5.0",
         "@playwright/test": "^1.51.0",
         "@sebastianwessel/surql-gen": "^2.7.1",
+        "@shikijs/monaco": "^3.2.1",
         "@skeletonlabs/skeleton": "3.0.0",
         "@skeletonlabs/skeleton-svelte": "1.0.0",
         "@sveltejs/adapter-auto": "^4.0.0",
@@ -178,6 +181,8 @@
 
     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
 
+    "@monaco-editor/loader": ["@monaco-editor/loader@1.5.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw=="],
+
     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
 
     "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@@ -240,6 +245,8 @@
 
     "@shikijs/langs": ["@shikijs/langs@3.2.1", "", { "dependencies": { "@shikijs/types": "3.2.1" } }, "sha512-If0iDHYRSGbihiA8+7uRsgb1er1Yj11pwpX1c6HLYnizDsKAw5iaT3JXj5ZpaimXSWky/IhxTm7C6nkiYVym+A=="],
 
+    "@shikijs/monaco": ["@shikijs/monaco@3.2.1", "", { "dependencies": { "@shikijs/core": "3.2.1", "@shikijs/types": "3.2.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-9XaRuwETRRhi+4g1EdMsK1dx1mHuL1XnXWmDRFL2PkMrDIGqrzY9DGR+YnWlWuoEY0kU+vbCMxH7rog1yuWJvA=="],
+
     "@shikijs/themes": ["@shikijs/themes@3.2.1", "", { "dependencies": { "@shikijs/types": "3.2.1" } }, "sha512-k5DKJUT8IldBvAm8WcrDT5+7GA7se6lLksR+2E3SvyqGTyFMzU2F9Gb7rmD+t+Pga1MKrYFxDIeyWjMZWM6uBQ=="],
 
     "@shikijs/types": ["@shikijs/types@3.2.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA=="],
@@ -876,6 +883,8 @@
 
     "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
 
+    "monaco-editor": ["monaco-editor@0.52.2", "", {}, "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ=="],
+
     "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
 
     "mrmime": ["mrmime@2.0.0", "", {}, "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw=="],
@@ -1064,6 +1073,8 @@
 
     "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
 
+    "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="],
+
     "std-env": ["std-env@3.8.0", "", {}, "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w=="],
 
     "streamx": ["streamx@2.22.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw=="],
diff --git a/db/schema/schema.surql b/db/schema/schema.surql
index 687002ae..ab29d0a3 100644
--- a/db/schema/schema.surql
+++ b/db/schema/schema.surql
@@ -54,12 +54,28 @@ DEFINE TABLE evaluation TYPE RELATION IN tutor OUT submission SCHEMALESS PERMISS
 
 DEFINE FIELD score ON evaluation TYPE option<float> PERMISSIONS FULL;
 DEFINE FIELD final ON evaluation TYPE bool DEFAULT false PERMISSIONS FULL;
+-- NOTE: making this an edge between evaluation->submission seems overkill
+-- NOTE: but maybe make this a record reference? (https://surrealdb.com/docs/surrealql/datamodel/references)
+DEFINE FIELD annotations ON evaluation TYPE array<record<annotation>> DEFAULT [] PERMISSIONS FULL; 
 
 DEFINE INDEX one_per_submission ON evaluation FIELDS out UNIQUE;
 
 -- DEFINE EVENT ...
 
 
+-- ------------------------------
+-- TABLE: annotation
+-- ------------------------------
+
+DEFINE TABLE annotation TYPE NORMAL SCHEMALESS PERMISSIONS NONE;
+DEFINE FIELD color ON annotation TYPE option<string> PERMISSIONS FULL;
+DEFINE FIELD content ON annotation TYPE string PERMISSIONS FULL;
+-- TODO data schema for position
+-- * can apply to a range (or ranges?) in the text source of the submission, shown at the range it applies to, OR
+-- * can apply to the submission as a whole, shown in a dedicated window/list/view on the submission
+-- DEFINE FIELD 
+
+
 -- ------------------------------
 -- TABLE: exercise_group
 -- ------------------------------
diff --git a/devenv.lock b/devenv.lock
index 7a45f6f5..d9e34d9a 100644
--- a/devenv.lock
+++ b/devenv.lock
@@ -114,31 +114,10 @@
         "type": "github"
       }
     },
-    "git-hooks": {
-      "inputs": {
-        "flake-compat": "flake-compat",
-        "gitignore": "gitignore",
-        "nixpkgs": [
-          "nixpkgs"
-        ]
-      },
-      "locked": {
-        "lastModified": 1741379162,
-        "owner": "cachix",
-        "repo": "git-hooks.nix",
-        "rev": "b5a62751225b2f62ff3147d0a334055ebadcd5cc",
-        "type": "github"
-      },
-      "original": {
-        "owner": "cachix",
-        "repo": "git-hooks.nix",
-        "type": "github"
-      }
-    },
     "gitignore": {
       "inputs": {
         "nixpkgs": [
-          "git-hooks",
+          "pre-commit-hooks",
           "nixpkgs"
         ]
       },
@@ -234,17 +213,35 @@
         "type": "github"
       }
     },
+    "pre-commit-hooks": {
+      "inputs": {
+        "flake-compat": "flake-compat",
+        "gitignore": "gitignore",
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1741379162,
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "rev": "b5a62751225b2f62ff3147d0a334055ebadcd5cc",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "type": "github"
+      }
+    },
     "root": {
       "inputs": {
         "devenv": "devenv",
-        "git-hooks": "git-hooks",
         "mk-shell-bin": "mk-shell-bin",
         "nix2container": "nix2container",
         "nixpkgs": "nixpkgs",
         "nixpkgs-unstable": "nixpkgs-unstable",
-        "pre-commit-hooks": [
-          "git-hooks"
-        ],
+        "pre-commit-hooks": "pre-commit-hooks",
         "surrealdb-git": "surrealdb-git"
       }
     },
diff --git a/src/lib/components/CodeBlock/AnnotationEditor.svelte b/src/lib/components/CodeBlock/AnnotationEditor.svelte
new file mode 100644
index 00000000..73be5afc
--- /dev/null
+++ b/src/lib/components/CodeBlock/AnnotationEditor.svelte
@@ -0,0 +1,147 @@
+<script lang="ts">
+	import { createEventDispatcher } from 'svelte';
+	import type { SelectionInfo, Annotation } from './types';
+
+	// Props
+	let {
+		selection = null
+	}: {
+		selection: SelectionInfo | null;
+	} = $props();
+
+	// Local state
+	let commentText = $state('');
+	let annotationColor = $state('#ffeb3b'); // Default yellow
+
+	// Event dispatcher
+	const dispatch = createEventDispatcher<{
+		save: Annotation;
+	}>();
+
+	// Handle save button click
+	function handleSave() {
+		if (!selection || selection.isCollapsed || !commentText.trim()) return;
+
+		const newAnnotation: Annotation = {
+			id: crypto.randomUUID(),
+			selection,
+			content: commentText.trim(),
+			color: annotationColor,
+			createdAt: new Date()
+		};
+
+		dispatch('save', newAnnotation);
+
+		// Reset the form
+		commentText = '';
+	}
+</script>
+
+<div class="annotation-editor">
+	<h3>Create Annotation</h3>
+
+	{#if selection && !selection.isCollapsed}
+		<div class="selected-text">
+			<strong>Selected Text:</strong>
+			<span class="text-snippet">{selection.text}</span>
+		</div>
+
+		<div class="position-info">
+			<strong>Position:</strong>
+			<span class="position-details">
+				Lines {selection.position?.start.line}-{selection.position?.end.line}, Columns {selection
+					.position?.start.column}-{selection.position?.end.column}
+			</span>
+		</div>
+
+		<div class="form-group">
+			<label for="comment">Comment:</label>
+			<textarea
+				id="comment"
+				bind:value={commentText}
+				placeholder="Add your annotation comment here..."
+				rows="3"
+			></textarea>
+		</div>
+
+		<div class="form-group">
+			<label for="color">Highlight Color:</label>
+			<input type="color" id="color" bind:value={annotationColor} />
+		</div>
+
+		<button class="save-btn" on:click={handleSave} disabled={!commentText.trim()}>
+			Save Annotation
+		</button>
+	{:else}
+		<p>Select text in the code to add an annotation</p>
+	{/if}
+</div>
+
+<style>
+	.annotation-editor {
+		margin-top: 1rem;
+		padding: 1rem;
+		border: 1px solid #ccc;
+		border-radius: 4px;
+		background-color: #f5f5f5;
+	}
+
+	.selected-text {
+		margin-bottom: 1rem;
+		padding: 0.5rem;
+		background-color: #e0e0e0;
+		border-radius: 4px;
+	}
+
+	.text-snippet {
+		font-family: monospace;
+		word-break: break-all;
+	}
+
+	.position-info {
+		margin-bottom: 1rem;
+		padding: 0.5rem;
+		background-color: #e0e0e0;
+		border-radius: 4px;
+		font-size: 0.9rem;
+	}
+
+	.position-details {
+		font-family: monospace;
+	}
+
+	.form-group {
+		margin-bottom: 1rem;
+	}
+
+	label {
+		display: block;
+		margin-bottom: 0.5rem;
+		font-weight: bold;
+	}
+
+	textarea {
+		width: 100%;
+		padding: 0.5rem;
+		border: 1px solid #ccc;
+		border-radius: 4px;
+	}
+
+	.save-btn {
+		padding: 0.5rem 1rem;
+		background-color: #4caf50;
+		color: white;
+		border: none;
+		border-radius: 4px;
+		cursor: pointer;
+	}
+
+	.save-btn:disabled {
+		background-color: #cccccc;
+		cursor: not-allowed;
+	}
+
+	.save-btn:not(:disabled):hover {
+		background-color: #45a049;
+	}
+</style>
diff --git a/src/lib/components/CodeBlock/AnnotationList.svelte b/src/lib/components/CodeBlock/AnnotationList.svelte
new file mode 100644
index 00000000..3faf5cdb
--- /dev/null
+++ b/src/lib/components/CodeBlock/AnnotationList.svelte
@@ -0,0 +1,137 @@
+<script lang="ts">
+	import { createEventDispatcher } from 'svelte';
+	import type { Annotation } from './types';
+
+	// Props
+	let {
+		annotations = []
+	}: {
+		annotations: Annotation[];
+	} = $props();
+
+	// Event dispatcher
+	const dispatch = createEventDispatcher<{
+		delete: string;
+		edit: Annotation;
+	}>();
+
+	// Format date
+	function formatDate(date: Date): string {
+		return new Intl.DateTimeFormat('en-US', {
+			year: 'numeric',
+			month: 'short',
+			day: 'numeric',
+			hour: '2-digit',
+			minute: '2-digit'
+		}).format(date);
+	}
+
+	// Handle deletion
+	function handleDelete(id: string) {
+		dispatch('delete', id);
+	}
+</script>
+
+<div class="annotations-list">
+	<h3>Annotations ({annotations.length})</h3>
+
+	{#if annotations.length === 0}
+		<p>No annotations yet</p>
+	{:else}
+		<ul>
+			{#each annotations as annotation (annotation.id)}
+				<li style="border-left-color: {annotation.color}">
+					<div class="annotation-text">"{annotation.selection.text}"</div>
+					<div class="annotation-position">
+						Position: Line {annotation.selection.position?.start.line}-{annotation.selection
+							.position?.end.line}, Column {annotation.selection.position?.start.column}-{annotation
+							.selection.position?.end.column}
+					</div>
+					<div class="annotation-comment">{annotation.content}</div>
+					<div class="annotation-meta">
+						Created: {formatDate(annotation.createdAt)}
+					</div>
+					<div class="annotation-actions">
+						<button
+							class="delete-btn"
+							on:click={() => handleDelete(annotation.id)}
+							title="Delete annotation"
+						>
+							Delete
+						</button>
+					</div>
+				</li>
+			{/each}
+		</ul>
+	{/if}
+</div>
+
+<style>
+	.annotations-list {
+		margin-top: 1rem;
+		padding: 1rem;
+		border: 1px solid #ccc;
+		border-radius: 4px;
+		background-color: #f5f5f5;
+	}
+
+	ul {
+		list-style: none;
+		padding: 0;
+		margin: 0;
+	}
+
+	li {
+		margin-bottom: 1rem;
+		padding: 0.75rem;
+		border-left: 4px solid #ccc;
+		background-color: white;
+		border-radius: 0 4px 4px 0;
+	}
+
+	.annotation-text {
+		font-family: monospace;
+		margin-bottom: 0.5rem;
+		font-style: italic;
+		word-break: break-all;
+	}
+
+	.annotation-position {
+		font-family: monospace;
+		font-size: 0.8rem;
+		background-color: #f0f0f0;
+		padding: 0.25rem 0.5rem;
+		border-radius: 2px;
+		margin-bottom: 0.5rem;
+		display: inline-block;
+	}
+
+	.annotation-comment {
+		margin-bottom: 0.5rem;
+	}
+
+	.annotation-meta {
+		font-size: 0.8rem;
+		color: #666;
+		margin-bottom: 0.5rem;
+	}
+
+	.annotation-actions {
+		display: flex;
+		justify-content: flex-end;
+	}
+
+	.delete-btn {
+		padding: 0.25rem 0.5rem;
+		background-color: #f44336;
+		color: white;
+		border: none;
+		border-radius: 4px;
+		cursor: pointer;
+		font-size: 0.8rem;
+	}
+
+	.delete-btn:hover {
+		background-color: #d32f2f;
+	}
+</style>
diff --git a/src/lib/components/CodeBlock/CodeBlock.svelte b/src/lib/components/CodeBlock/CodeBlock.svelte
index c763432f..b09869be 100644
--- a/src/lib/components/CodeBlock/CodeBlock.svelte
+++ b/src/lib/components/CodeBlock/CodeBlock.svelte
@@ -1,5 +1,3 @@
-<!-- @component Code Block based on: https://shiki.style/ -->
-
 <script module>
 	import { createHighlighterCoreSync } from 'shiki/core';
 	import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
@@ -12,71 +10,378 @@
 	import html from 'shiki/langs/html.mjs';
 	import css from 'shiki/langs/css.mjs';
 	import js from 'shiki/langs/javascript.mjs';
-
 	// https://shiki.style/guide/sync-usage
 	const shiki = createHighlighterCoreSync({
 		engine: createJavaScriptRegexEngine(),
-		// Implement your import theme.
 		themes: [themeDarkPlus],
-		// Implement your imported and supported languages.
 		langs: [console, html, css, js]
 	});
-
-	const lineNumberTransformer = {};
 </script>
 
 <script lang="ts">
-	import type { CodeBlockProps } from './types';
+	import type { CodeBlockProps, SelectionInfo } from './types';
+	import { addLineNumber, addTokenIds } from './transformers';
+	import { onMount, onDestroy } from 'svelte';
+	import { browser } from '$app/environment';
 
 	let {
 		code = '',
 		lang = 'console',
 		theme = 'dark-plus',
 		// Base Style Props
-		base = ' overflow-hidden',
+		base = 'overflow-hidden',
 		rounded = 'rounded-container',
 		shadow = '',
 		classes = '',
 		// Pre Style Props
 		preBase = '',
 		prePadding = '[&>pre]:p-4',
-		preClasses = ''
+		preClasses = '',
+		// Selection props
+		onSelectionChange = (selInfo: any) => {},
+		// Debug view
+		showDebug = false
 	}: CodeBlockProps = $props();
 
 	// Shiki convert to HTML
 	const generatedHtml = shiki.codeToHtml(code, {
 		lang,
 		theme,
-		transformers: [
-			{
-				// code(node) {
-				// this.addClassToHast(node, 'language-js');
-				// },
-				line(node, line) {
-					// Create a span element for the line number
-					const lineNumberSpan: Element = {
-						type: 'element',
-						tagName: 'span',
-						properties: {
-							className: ['line-number']
-						},
-						children: [{ type: 'text', value: `${line}` }]
+		transformers: [addLineNumber, addTokenIds]
+	});
+
+	// Selection state
+	let selectionInfo: SelectionInfo = $state({
+		anchorNode: null,
+		anchorOffset: 0,
+		focusNode: null,
+		focusOffset: 0,
+		isCollapsed: true,
+		rangeCount: 0,
+		type: '',
+		direction: '',
+		text: '',
+		position: {
+			start: {
+				line: 0,
+				column: 0
+			},
+			end: {
+				line: 0,
+				column: 0
+			}
+		}
+	});
+
+	// Code block element reference
+	let codeBlockEl: HTMLElement;
+
+	// Extract position (line, column) information from a node
+	function extractPositionInfo(
+		node: Node,
+		offset: number
+	): { line: number; column: number } | null {
+		// Default position if we can't determine
+		let position = { line: 0, column: 0 };
+
+		// Try to find the closest element with data-token attribute
+		let currentNode = node;
+		let tokenElement = null;
+
+		// If we're in a text node, get its parent
+		if (currentNode.nodeType === Node.TEXT_NODE && currentNode.parentElement) {
+			currentNode = currentNode.parentElement;
+		}
+
+		// First, check if the current element has data-token
+		if (currentNode instanceof Element && currentNode.hasAttribute('data-token')) {
+			tokenElement = currentNode;
+		} else {
+			// Walk up the tree to find an element with data-token
+			let el = currentNode instanceof Element ? currentNode : currentNode.parentElement;
+			while (el && !tokenElement) {
+				if (el.hasAttribute('data-token')) {
+					tokenElement = el;
+					break;
+				}
+
+				// Try to find it in previous siblings
+				let sibling = el.previousElementSibling;
+				while (sibling && !tokenElement) {
+					if (sibling.hasAttribute('data-token')) {
+						tokenElement = sibling;
+						break;
+					}
+					sibling = sibling.previousElementSibling;
+				}
+
+				el = el.parentElement;
+			}
+		}
+
+		// Extract position from data-token
+		if (tokenElement) {
+			const tokenAttr = tokenElement.getAttribute('data-token');
+			if (tokenAttr) {
+				const parts = tokenAttr.split(':');
+				if (parts.length >= 3 && parts[0] === 'token') {
+					position = {
+						line: parseInt(parts[1], 10),
+						column: parseInt(parts[2], 10)
 					};
 
-					// Add the line number span at the beginning of the line's children
-					if (node.children) {
-						node.children.unshift(lineNumberSpan);
+					// If we're in a text node, add the offset to column
+					if (node.nodeType === Node.TEXT_NODE) {
+						position.column += offset;
 					}
-				},
-				span(node, line, col) {
-					node.properties['data-token'] = `token:${line}:${col}`;
 				}
 			}
-		]
+		} else {
+			// Try to find the line number by looking for the line-number span
+			const lineEl = findClosestLineElement(currentNode);
+			if (lineEl) {
+				const lineNumberEl = lineEl.querySelector('.line-number');
+				if (lineNumberEl && lineNumberEl.textContent) {
+					position.line = parseInt(lineNumberEl.textContent, 10);
+
+					// Estimate column based on text content before the selection
+					if (node.nodeType === Node.TEXT_NODE) {
+						// Count characters up to the offset
+						position.column = offset;
+					}
+				}
+			}
+		}
+
+		return position;
+	}
+
+	// Find the closest line element (container for a line of code)
+	function findClosestLineElement(node: Node): Element | null {
+		let currentNode = node;
+
+		// If text node, start with parent
+		if (currentNode.nodeType === Node.TEXT_NODE && currentNode.parentElement) {
+			currentNode = currentNode.parentElement;
+		}
+
+		// Look for a line element (assume it's a direct child of pre)
+		while (currentNode && currentNode.parentElement) {
+			const parent = currentNode.parentElement;
+			// If parent is a pre element, this is likely a line element
+			if (parent.tagName === 'PRE' && currentNode instanceof Element) {
+				return currentNode as Element;
+			}
+			currentNode = parent;
+		}
+
+		return null;
+	}
+
+	// Handle selection change
+	function updateSelectionInfo() {
+		const selection = window.getSelection();
+		if (!selection) return;
+
+		// Check if the selection is within our code block
+		if (!isSelectionInCodeBlock(selection)) return;
+
+		// Extract position information
+		const startPosition = selection.anchorNode
+			? extractPositionInfo(selection.anchorNode, selection.anchorOffset)
+			: { line: 0, column: 0 };
+
+		const endPosition = selection.focusNode
+			? extractPositionInfo(selection.focusNode, selection.focusOffset)
+			: { line: 0, column: 0 };
+
+		// Determine actual start and end points based on selection direction
+		let position = {
+			start: { ...startPosition },
+			end: { ...endPosition }
+		};
+
+		// If selection is backwards, swap start and end
+		if (selection.anchorNode && selection.focusNode) {
+			// Compare positions to determine actual start/end
+			if (
+				startPosition.line > endPosition.line ||
+				(startPosition.line === endPosition.line && startPosition.column > endPosition.column)
+			) {
+				position = {
+					start: { ...endPosition },
+					end: { ...startPosition }
+				};
+			}
+		}
+
+		selectionInfo = {
+			anchorNode: selection.anchorNode ? describeNode(selection.anchorNode) : null,
+			anchorOffset: selection.anchorOffset,
+			focusNode: selection.focusNode ? describeNode(selection.focusNode) : null,
+			focusOffset: selection.focusOffset,
+			isCollapsed: selection.isCollapsed,
+			rangeCount: selection.rangeCount,
+			type: selection.type,
+			direction: selection.direction,
+			text: selection.toString(),
+			position
+		};
+
+		// Call the onSelectionChange callback if provided
+		onSelectionChange(selectionInfo);
+	}
+
+	// Helper to check if selection is within our code block
+	function isSelectionInCodeBlock(selection: Selection): boolean {
+		if (!selection.rangeCount) return false;
+
+		const range = selection.getRangeAt(0);
+		return codeBlockEl.contains(range.commonAncestorContainer);
+	}
+
+	// Helper to describe a DOM node in a readable way
+	function describeNode(node: Node): string {
+		if (node.nodeType === Node.TEXT_NODE) {
+			return `TEXT: "${node.textContent?.slice(0, 20)}${node.textContent && node.textContent.length > 20 ? '...' : ''}"`;
+		} else if (node.nodeType === Node.ELEMENT_NODE) {
+			const el = node as Element;
+			return `ELEMENT: <${el.tagName.toLowerCase()}${el.id ? ' id="' + el.id + '"' : ''}${el.className ? ' class="' + el.className + '"' : ''}>`;
+		} else {
+			return `NODE: Type ${node.nodeType}`;
+		}
+	}
+
+	onMount(() => {
+		// Add global mouseup event to capture selections outside the immediate code block
+		document.addEventListener('mouseup', updateSelectionInfo);
+		document.addEventListener('keyup', updateSelectionInfo);
+	});
+
+	onDestroy(() => {
+		if (!browser) return;
+		// Clean up event listeners
+		document.removeEventListener('mouseup', updateSelectionInfo);
+		document.removeEventListener('keyup', updateSelectionInfo);
 	});
 </script>
 
-<div class="{base} {rounded} {shadow} {classes} {preBase} {prePadding} {preClasses}">
+<div
+	bind:this={codeBlockEl}
+	class="{base} {rounded} {shadow} {classes} {preBase} {prePadding} {preClasses} code-block-selectable"
+>
 	<!-- Output Shiki's Generated HTML -->
 	{@html generatedHtml}
 </div>
+
+<!-- Selection Debug View -->
+{#if showDebug}
+	<div class="selection-debug">
+		<h3>Selection Debug</h3>
+		{#if !selectionInfo.isCollapsed}
+			<div class="selection-info">
+				<div class="info-row">
+					<span class="label">Selected Text:</span>
+					<span class="value text-value">{selectionInfo.text}</span>
+				</div>
+				<div class="info-row">
+					<span class="label">Type:</span>
+					<span class="value">{selectionInfo.type}</span>
+				</div>
+				<div class="info-row">
+					<span class="label">Direction:</span>
+					<span class="value">{selectionInfo.direction}</span>
+				</div>
+				<div class="info-row">
+					<span class="label">Range Count:</span>
+					<span class="value">{selectionInfo.rangeCount}</span>
+				</div>
+				<div class="info-row">
+					<span class="label">Is Collapsed:</span>
+					<span class="value">{String(selectionInfo.isCollapsed)}</span>
+				</div>
+				<div class="info-row">
+					<span class="label">Anchor Node:</span>
+					<span class="value">{selectionInfo.anchorNode}</span>
+				</div>
+				<div class="info-row">
+					<span class="label">Anchor Offset:</span>
+					<span class="value">{selectionInfo.anchorOffset}</span>
+				</div>
+				<div class="info-row">
+					<span class="label">Focus Node:</span>
+					<span class="value">{selectionInfo.focusNode}</span>
+				</div>
+				<div class="info-row">
+					<span class="label">Focus Offset:</span>
+					<span class="value">{selectionInfo.focusOffset}</span>
+				</div>
+				<div class="info-row">
+					<span class="label">Position:</span>
+					<span class="value">
+						Start: Line {selectionInfo.position.start.line}, Column {selectionInfo.position.start
+							.column} | End: Line {selectionInfo.position.end.line}, Column {selectionInfo.position
+							.end.column}
+					</span>
+				</div>
+			</div>
+		{:else}
+			<p>No selection active. Click and drag to select text in the code block.</p>
+		{/if}
+	</div>
+{/if}
+
+<style>
+	.code-block-selectable :global(pre) {
+		user-select: text;
+		-webkit-user-select: text;
+		-moz-user-select: text;
+		-ms-user-select: text;
+		cursor: text;
+	}
+
+	.selection-debug {
+		margin-top: 1rem;
+		padding: 1rem;
+		border: 1px solid #ccc;
+		border-radius: 4px;
+		background-color: #f5f5f5;
+	}
+
+	h3 {
+		margin-top: 0;
+		margin-bottom: 1rem;
+	}
+
+	.selection-info {
+		display: grid;
+		grid-template-columns: 1fr;
+		gap: 0.5rem;
+	}
+
+	.info-row {
+		display: grid;
+		grid-template-columns: 150px 1fr;
+		gap: 0.5rem;
+	}
+
+	.label {
+		font-weight: bold;
+	}
+
+	.text-value {
+		font-family: monospace;
+		word-break: break-all;
+		background-color: #e0e0e0;
+		padding: 0.25rem;
+		border-radius: 2px;
+	}
+
+	:global(.line-number) {
+		display: inline-block;
+		width: 1.5rem;
+		margin-right: 0.5rem;
+		color: #888;
+		text-align: right;
+	}
+</style>
diff --git a/src/lib/components/CodeBlock/transformers.ts b/src/lib/components/CodeBlock/transformers.ts
index 75a3e5b6..15c3e8fd 100644
--- a/src/lib/components/CodeBlock/transformers.ts
+++ b/src/lib/components/CodeBlock/transformers.ts
@@ -1,21 +1,48 @@
-import { codeToHtml } from 'shiki';
-import { type Element } from 'hast';
+import type { ShikiTransformer } from 'shiki';
+import type * as Hast from 'hast';
 
-const code = await codeToHtml('foo\bar', {
-	lang: 'js',
-	theme: 'vitesse-dark',
-	transformers: [
-		{
-			line(node, line) {
-				const lineNumberSpan: Element = {
-					type: 'element',
-					tagName: 'span',
-					properties: {
-						className: ['line-number']
-					},
-					children: [{ type: 'text', value: `${line}` }]
-				};
-			}
+/**
+ * Adds line numbers to the code display
+ */
+export const addLineNumber: ShikiTransformer = {
+	line(node, line) {
+		// Create a span element for the line number
+		const lineNumberSpan: Hast.Element = {
+			type: 'element',
+			tagName: 'span',
+			properties: {
+				className: ['line-number']
+			},
+			children: [{ type: 'text', value: `${line}` }]
+		};
+
+		// Add the line number span at the beginning of the line's children
+		if (node.children) {
+			node.children.unshift(lineNumberSpan);
 		}
-	]
-});
+	}
+};
+
+/**
+ * Adds data attributes to each token span to help with selection mapping
+ */
+export const addTokenIds: ShikiTransformer = {
+	code(node) {
+		// Add a class to the code element for easier selection
+		node.properties.className = [...(node.properties.className || []), 'annotatable-code'];
+	},
+
+	line(node, line) {
+		// Add a data attribute for the line number to help with selection mapping
+		node.properties['data-line'] = `${line}`;
+	},
+
+	span(node, line, col) {
+		// Add a data attribute to help with selection mapping
+		node.properties['data-token'] = `token:${line}:${col}`;
+
+		// Add the line and column as separate attributes for easier access
+		node.properties['data-line'] = `${line}`;
+		node.properties['data-column'] = `${col}`;
+	}
+};
diff --git a/src/lib/components/CodeBlock/types.ts b/src/lib/components/CodeBlock/types.ts
index d086bc6c..a5de7646 100644
--- a/src/lib/components/CodeBlock/types.ts
+++ b/src/lib/components/CodeBlock/types.ts
@@ -1,7 +1,10 @@
+/**
+ * Props for the CodeBlock component
+ */
 export interface CodeBlockProps {
 	code?: string;
-	lang?: 'console' | 'html' | 'css' | 'js';
-	theme?: 'dark-plus';
+	lang?: string;
+	theme?: string;
 	// Base Style Props
 	base?: string;
 	rounded?: string;
@@ -11,4 +14,64 @@ export interface CodeBlockProps {
 	preBase?: string;
 	prePadding?: string;
 	preClasses?: string;
+	// Selection props
+	onSelectionChange?: (selInfo: SelectionInfo) => void;
+	// Debug view
+	showDebug?: boolean;
+}
+
+/**
+ * Information about a DOM Selection
+ */
+export interface SelectionInfo {
+	anchorNode: string | null;
+	anchorOffset: number;
+	focusNode: string | null;
+	focusOffset: number;
+	isCollapsed: boolean;
+	rangeCount: number;
+	type: string;
+	direction: string;
+	text: string;
+	position?: {
+		start: {
+			line: number;
+			column: number;
+		};
+		end: {
+			line: number;
+			column: number;
+		};
+	};
+}
+
+/**
+ * Represents a position in the text source
+ */
+export interface SrcPosition {
+	column: number;
+	line: number;
+}
+
+/**
+ * Represents a range in the text source
+ * startLineNumber, startColumn <= endLineNumber, endColumn
+ */
+export interface SrcRange {
+	start: SrcPosition;
+	end: SrcPosition;
+}
+
+/**
+ * Represents an annotation added to the code
+ */
+export interface Annotation {
+	id: string;
+	// TODO completely replace selection with position
+	selection: SelectionInfo;
+	// should be translated from/to a DOM Selection
+	appliesTo: SrcRange;
+	content?: string;
+	color?: string;
+	createdAt: Date;
 }
diff --git a/src/routes/(app)/debug/editor/+page.svelte b/src/routes/(app)/debug/editor/+page.svelte
index 186a0946..f2316414 100644
--- a/src/routes/(app)/debug/editor/+page.svelte
+++ b/src/routes/(app)/debug/editor/+page.svelte
@@ -1,8 +1,144 @@
 <script lang="ts">
-	import Editor from '$lib/components/Editor/Editor.svelte';
+	import { onMount } from 'svelte';
+	import SelectableCodeBlock from '$lib/components/CodeBlock/CodeBlock.svelte';
+	import AnnotationEditor from '$lib/components/CodeBlock/AnnotationEditor.svelte';
+	import AnnotationsList from '$lib/components/CodeBlock/AnnotationList.svelte';
+	import type { SelectionInfo, Annotation } from '$lib/components/CodeBlock/types';
+
+	const sampleCode = `function calculateTotal(items) {
+  return items
+    .map(item => item.price * item.quantity)
+    .reduce((total, itemTotal) => total + itemTotal, 0);
+}
+
+const cart = [
+  { name: 'Keyboard', price: 49.99, quantity: 1 },
+  { name: 'Mouse', price: 29.99, quantity: 1 },
+  { name: 'Monitor', price: 199.99, quantity: 2 }
+];
+
+const total = calculateTotal(cart);
+console.log(\`Total: \$\${total.toFixed(2)}\`);`;
+
+	// Store the current selection info
+	let currentSelection: SelectionInfo | null = null;
+
+	// Store annotations
+	let annotations: Annotation[] = [];
+
+	// Show debug panel toggle
+	let showDebug = false;
+
+	// Handle selection changes
+	function handleSelectionChange(selInfo: SelectionInfo) {
+		currentSelection = selInfo;
+	}
+
+	// Save annotation
+	function handleSaveAnnotation(event: CustomEvent<Annotation>) {
+		const newAnnotation = event.detail;
+		annotations = [...annotations, newAnnotation];
+		saveAnnotationsToStorage();
+	}
+
+	// Delete annotation
+	function handleDeleteAnnotation(event: CustomEvent<string>) {
+		const idToDelete = event.detail;
+		annotations = annotations.filter((a) => a.id !== idToDelete);
+		saveAnnotationsToStorage();
+	}
+
+	// Save annotations to localStorage
+	function saveAnnotationsToStorage() {
+		localStorage.setItem('codeAnnotations', JSON.stringify(annotations));
+	}
+
+	// Load annotations from localStorage on mount
+	onMount(() => {
+		const savedAnnotations = localStorage.getItem('codeAnnotations');
+		if (savedAnnotations) {
+			try {
+				const parsed = JSON.parse(savedAnnotations);
+				// Convert string dates back to Date objects
+				annotations = parsed.map((a: any) => ({
+					...a,
+					createdAt: new Date(a.createdAt)
+				}));
+			} catch (error) {
+				console.error('Failed to load annotations from storage', error);
+			}
+		}
+	});
 </script>
 
-<Editor />
+<main>
+	<h1>Code Annotation Editor</h1>
+
+	<div class="code-container">
+		<SelectableCodeBlock
+			code={sampleCode}
+			lang="js"
+			onSelectionChange={handleSelectionChange}
+			{showDebug}
+		/>
+	</div>
+
+	<div class="controls">
+		<label class="debug-toggle">
+			<input type="checkbox" bind:checked={showDebug} />
+			Show Selection Debug
+		</label>
+	</div>
+
+	<div class="editor-container">
+		<div class="annotation-tools">
+			<AnnotationEditor selection={currentSelection} on:save={handleSaveAnnotation} />
+		</div>
+
+		<div class="annotations-container">
+			<AnnotationsList {annotations} on:delete={handleDeleteAnnotation} />
+		</div>
+	</div>
+</main>
 
 <style>
+	main {
+		max-width: 800px;
+		margin: 0 auto;
+		padding: 2rem;
+	}
+
+	h1 {
+		margin-bottom: 2rem;
+	}
+
+	.code-container {
+		border: 1px solid #ddd;
+		border-radius: 4px;
+		margin-bottom: 1rem;
+	}
+
+	.controls {
+		margin-bottom: 1rem;
+	}
+
+	.debug-toggle {
+		display: flex;
+		align-items: center;
+		gap: 0.5rem;
+		font-size: 0.9rem;
+		cursor: pointer;
+	}
+
+	.editor-container {
+		display: grid;
+		grid-template-columns: 1fr 1fr;
+		gap: 1rem;
+	}
+
+	@media (max-width: 768px) {
+		.editor-container {
+			grid-template-columns: 1fr;
+		}
+	}
 </style>
-- 
GitLab