master
svelte 352 lines 11.6 KB
Raw
1 <script lang="ts">
2 import {keymap, drawSelection, highlightActiveLine, dropCursor,
3 rectangularSelection, crosshairCursor,
4 lineNumbers, highlightActiveLineGutter, EditorView} from "@codemirror/view";
5 import {EditorState, Compartment} from "@codemirror/state"
6 import {defaultKeymap, history, indentWithTab } from "@codemirror/commands";
7 import { onMount, tick } from "svelte";
8 import { writable } from "svelte/store";
9 import { saveFile, updateSaveState } from "./File";
10 import { appSettings } from "../config/config";
11 import { warn } from "tauri-plugin-log-api";
12 import { foldGutter, bracketMatching, indentUnit, syntaxHighlighting } from "@codemirror/language";
13 import { closeBrackets } from "@codemirror/autocomplete";
14 import { indentationMarkers } from '@replit/codemirror-indentation-markers';
15 import { editorHighlightStyle, editorTheme, getThemeProperty, is_dark_theme } from "../config/themehandler";
16 import { autocompletion } from "@codemirror/autocomplete";
17
18
19
20 let ref;
21 let editorView: EditorView;
22 export let hidden = false;
23 export let content = "";
24 const file_info = writable({
25 "filename": "",
26 "path": "",
27 "fileType": "",
28 "language": "",
29 "encoding": "",
30 "hasBom": false,
31 "spaces": 0,
32 "readonly": false,
33 });
34 const lang = new Compartment();
35 const tabSize = new Compartment();
36 const indentSize = new Compartment();
37 const colorScheme = new Compartment();
38 const theme = new Compartment();
39 const fontFamily = new Compartment();
40 const fontSize = new Compartment();
41
42 export function updateFileInfo(file) {
43 file_info.set(file);
44 }
45 export function getFileInfo() {
46 return $file_info;
47 }
48 export function getFileContent() {
49 return content;
50 }
51 export async function getLang(ext) {
52 if (ext === "txt") {
53 return "Plain Text";
54 }
55 let language = getLangFromExt(ext);
56 if (language) {
57 const mode = await language.load();
58 if (!mode) {
59 warn(`Syntax highlighting not supported for ${language.name}.`, {file: "Editor.svelte", line: 46});
60 }
61 else {
62 setLangMode(mode);
63 }
64 return language.name;
65 }
66 warn(`Language not found or extension "${ext}" not supported.`, {file: "Editor.svelte", line: 53});
67 return "Unknown";
68 }
69 function setLangMode(mode) {
70 editorView.dispatch({
71 effects: lang.reconfigure(mode)
72 })
73 }
74 export function getEncoding() {
75 return $file_info.encoding;
76 }
77 export function hasBom() {
78 return $file_info.hasBom;
79 }
80 export function getSpaces() {
81 return $file_info.spaces;
82 }
83 export function getView() {
84 return editorView;
85 }
86 export function setTabSize(size) {
87 spaces.set(size);
88 editorView.dispatch({
89 effects: tabSize.reconfigure(EditorState.tabSize.of(size))
90 })
91 setIndentSize(size)
92 }
93 function setIndentSize(size) {
94 editorView.dispatch({
95 effects: indentSize.reconfigure(indentUnit.of(" ".repeat(size)))
96 })
97 }
98 export function setScheme(scheme) {
99 editorView.dispatch({
100 effects: colorScheme.reconfigure(EditorView.darkTheme.of(scheme))
101 })
102 }
103 export function setTheme() {
104 editorView.dispatch({
105 effects: theme.reconfigure([get(editorTheme), syntaxHighlighting(get(editorHighlightStyle))])
106 })
107 }
108 export function setFontFamily(family: string) {
109 editorView.dispatch({
110 effects: fontFamily.reconfigure(EditorView.theme(
111 {
112 "*": { fontFamily: family }
113 }))
114 })
115 }
116 export function setFontSize(size: number) {
117 editorView.dispatch({
118 effects: fontSize.reconfigure(EditorView.theme(
119 {
120 "*": { fontSize: `${size}px` }
121 }))
122 })
123 }
124
125 onMount(async () => {
126 editorView = new EditorView({
127 parent: ref,
128 state: EditorState.create({
129 extensions: [
130 lineNumbers(),
131 foldGutter({openText:"", closedText: ""}),
132 bracketMatching(),
133 closeBrackets(),
134 highlightActiveLineGutter(),
135 //highlightSpecialChars(), // disabled as an attempt to hide bom for now
136 highlightActiveLine(),
137 dropCursor(),
138 history(),
139 drawSelection(),
140 crosshairCursor(),
141 rectangularSelection(),
142 autocompletion({
143 aboveCursor: true,
144 closeOnBlur: false
145 }),
146 indentSize.of(indentUnit.of(" ")),
147 indentationMarkers({
148 thickness: 1,
149 colors: {
150 dark: getThemeProperty("editor-gutterForeground"),
151 activeDark: getThemeProperty("editor-activeIndentLineColor"),
152 light: getThemeProperty("editor-gutterForeground"),
153 activeLight: getThemeProperty("editor-activeIndentLineColor")
154 }
155 }),
156 keymap.of([indentWithTab]),
157 lang.of([]),
158 EditorState.allowMultipleSelections.of(true),
159 theme.of([]),
160 fontFamily.of([]),
161 fontSize.of([]),
162 colorScheme.of(EditorView.darkTheme.of($is_dark_theme)),
163 keymap.of([
164 ...defaultKeymap
165 ]),
166 EditorView.updateListener.of(async update => {
167 if (update.docChanged) {
168 await updateContent();
169 }
170 if (update.state.selection.ranges.some(r => !r.empty)) {
171 updateLineInfo();
172 }
173 }),
174 tabSize.of(EditorState.tabSize.of(4))
175 ],
176 doc: content
177 })
178 })
179 editorView.contentDOM.classList.add("mousetrap");
180 setTheme();
181 setFontSize(await appSettings.get("editor.fontSize"));
182 setFontFamily(await appSettings.get("editor.fontFamily"));
183 setEditorLineHeight(await appSettings.get("editor.lineHeight"));
184 });
185
186 let _ = null;
187 async function updateContent() {
188 await tick();
189 content = editorView.state.doc.toString();
190 if (await appSettings.get("editor.autosave") === "true") {
191 clearTimeout(_);
192 _ = setTimeout(async () => {
193 if (!$file_info.path || $file_info.path === $file_info.filename || $file_info.path === "") {
194 console.warn("No path found. Cannot save");
195 return;
196 }
197 else {
198 await saveFile();
199 }
200 }, 1000)
201 }
202 else {
203 updateSaveState(false);
204 }
205 updateLineInfo();
206 }
207 export async function focus() {
208 // takes two ticks to focus for some reason
209 await tick();
210 await tick();
211 editorView.focus();
212 updateLineInfo();
213 language.set($file_info.language);
214 encoding.set({value: $file_info.encoding, hasBom: $file_info.hasBom});
215 setTabSize($file_info.spaces);
216 }
217 export function updateLineInfo() {
218 let lineNumber = editorView.state.doc.lineAt(editorView.state.selection.main.head).number;
219 let columnNumber = editorView.state.selection.ranges[0].head - editorView.state.doc.lineAt(editorView.state.selection.main.head).from;
220 line_info.set({line: lineNumber.toString(), column: (columnNumber + 1).toString()})
221 }
222
223 async function handleKeyDown(e) {
224 let key = e.code;
225 switch(key) {
226 case "ArrowRight": case "ArrowLeft": case "ArrowDown": case "ArrowUp":
227 updateLineInfo();
228 }
229 }
230 </script>
231 <script lang="ts" context="module">
232 import { languages as cmLangs } from "@codemirror/language-data";
233 import { tabs } from "./EditorTabList.svelte";
234 import { get } from "svelte/store";
235
236 export const line_info = writable({line: "-", column: "-"});
237 export const language = writable("Unknown");
238 export const encoding = writable({value: "UTF-8", hasBom: false});
239 export const spaces = writable(4);
240 const langmode = writable(null);
241
242 export function getLangFromExt(ext: string) {
243 let cmlang = cmLangs.find(l => l.extensions.includes(ext));
244 return cmlang;
245 }
246 export function setEditorFontSize(value: number) {
247 for (const tab of get(tabs)) {
248 if (tab.isfile) {
249 tab.content.setFontSize(value);
250 }
251 }
252 }
253 export function setEditorFontFamily(family: string) {
254 for (const tab of get(tabs)) {
255 if (tab.isfile) {
256 tab.content.setFontFamily(family);
257 }
258 }
259 }
260 export function setEditorLineHeight(height: string) {
261 const editors = document.querySelectorAll(".cm-scroller");
262 for (const editorContainer of editors) {
263 (editorContainer as HTMLElement).style.setProperty("line-height", height, "important")
264 }
265 }
266 export function setEditorTabSize(size) {
267 for (const tab of get(tabs)) {
268 if (tab.isfile) {
269 tab.content.setTabSize(size);
270 }
271 }
272 }
273 export function setColorScheme() {
274 for (const tab of get(tabs)) {
275 if (tab.isfile) {
276 tab.content.setScheme(get(is_dark_theme))
277 }
278 }
279 }
280 export function setEditorTheme() {
281 for (const tab of get(tabs)) {
282 if (tab.isfile) {
283 tab.content.setTheme();
284 }
285 }
286 }
287 </script>
288
289 <div bind:this={ref} class="editor" class:hidden on:mousedown={updateLineInfo} on:keydown={handleKeyDown}></div>
290
291 <style lang="scss">
292 .editor {
293 height: 100%;
294 width: 100%;
295 }
296 .hidden {
297 display: none;
298 }
299 :global(.cm-editor:focus),
300 :global(.cm-focused) {
301 outline: none !important;
302 }
303 :global(.cm-editor),
304 :global(.cm-wrap) {
305 height: 100%;
306 }
307 :global(.cm-scroller) {
308 font-size: 14px !important;
309 line-height: 1.3 !important;
310 padding-top: 7px;
311 overflow-y: overlay;
312 overflow-x: overlay !important;
313 width: -webkit-fill-available;
314 height: -webkit-fill-available !important;
315 position: absolute !important;
316 }
317 :global(.cm-gutters) {
318 z-index: 2;
319 }
320 :global(.cm-content) {
321 padding: 0 10px 200px 0 !important;
322 }
323 :global(.cm-lineNumbers) {
324 min-width: 60px !important;
325 &:hover {
326 ~:global(.cm-foldGutter) {
327 opacity: 100;
328 pointer-events: all;
329 }
330 }
331 }
332 :global(.cm-gutterElement) {
333 text-align: center !important;
334 }
335 :global(.cm-foldGutter) {
336 position: absolute;
337 left: 49px;
338 pointer-events: none;
339 opacity: 0;
340 transition: 0.4s;
341 &:hover {
342 opacity: 100;
343 pointer-events: all;
344 }
345 }
346 :global(.cm-indent-markers)::before {
347 z-index: 0 !important;
348 }
349 :global(.cm-line) {
350 padding: 0 2px 0 1px !important;
351 }
352 </style>