@@ -14,7 +14,7 @@ import {
1414
1515import { indentUnit } from "@codemirror/language" ;
1616import { search } from "@codemirror/search" ;
17- import { Compartment , EditorState , StateEffect } from "@codemirror/state" ;
17+ import { Compartment , EditorState , Prec , StateEffect } from "@codemirror/state" ;
1818import { oneDark } from "@codemirror/theme-one-dark" ;
1919import {
2020 EditorView ,
@@ -26,6 +26,8 @@ import {
2626} from "@codemirror/view" ;
2727import {
2828 abbreviationTracker ,
29+ EmmetKnownSyntax ,
30+ emmetCompletionSource ,
2931 emmetConfig ,
3032 expandAbbreviation ,
3133 wrapWithAbbreviation ,
@@ -321,6 +323,30 @@ async function EditorManager($header, $body) {
321323 return exts ;
322324 }
323325
326+ function createEmmetExtensionSet ( {
327+ syntax = EmmetKnownSyntax . html ,
328+ tracker = { } ,
329+ config : emmetOverrides = { } ,
330+ } = { } ) {
331+ const trackerExtension = abbreviationTracker ( {
332+ syntax,
333+ ...tracker ,
334+ } ) ;
335+ const { autocompleteTab = [ "markup" , "stylesheet" ] , ...restOverrides } =
336+ emmetOverrides || { } ;
337+ const emmetConfigExtension = emmetConfig . of ( {
338+ syntax,
339+ autocompleteTab,
340+ ...restOverrides ,
341+ } ) ;
342+ return [
343+ Prec . high ( trackerExtension ) ,
344+ wrapWithAbbreviation ( ) ,
345+ keymap . of ( [ { key : "Mod-e" , run : expandAbbreviation } ] ) ,
346+ emmetConfigExtension ,
347+ ] ;
348+ }
349+
324350 function applyOptions ( keys ) {
325351 const filter = keys ? new Set ( keys ) : null ;
326352 for ( const spec of cmOptionSpecs ) {
@@ -341,10 +367,32 @@ async function EditorManager($header, $body) {
341367 }
342368 }
343369
370+ // Plugin already wires CSS completions; attach extras for related syntaxes.
371+ const emmetCompletionSyntaxes = new Set ( [
372+ EmmetKnownSyntax . scss ,
373+ EmmetKnownSyntax . less ,
374+ EmmetKnownSyntax . sass ,
375+ EmmetKnownSyntax . sss ,
376+ EmmetKnownSyntax . stylus ,
377+ EmmetKnownSyntax . postcss ,
378+ ] ) ;
379+
380+ function maybeAttachEmmetCompletions ( targetExtensions , syntax ) {
381+ if ( emmetCompletionSyntaxes . has ( syntax ) ) {
382+ targetExtensions . push (
383+ EditorState . languageData . of ( ( ) => [
384+ { autocomplete : emmetCompletionSource } ,
385+ ] ) ,
386+ ) ;
387+ }
388+ }
389+
344390 // Create minimal CodeMirror editor
345391 const editorState = EditorState . create ( {
346392 doc : "" ,
347393 extensions : [
394+ // Emmet needs highest precedence so place before default keymaps
395+ ...createEmmetExtensionSet ( { syntax : EmmetKnownSyntax . html } ) ,
348396 ...createBaseExtensions ( ) ,
349397 getCommandKeymapExtension ( ) ,
350398 // Default theme
@@ -355,10 +403,6 @@ async function EditorManager($header, $body) {
355403 readOnlyCompartment . of ( EditorState . readOnly . of ( false ) ) ,
356404 // Editor options driven by settings via compartments
357405 ...getBaseExtensionsFromOptions ( ) ,
358- // Emmet abbreviation tracker and common keybindings
359- abbreviationTracker ( ) ,
360- wrapWithAbbreviation ( ) ,
361- keymap . of ( [ { key : "Mod-e" , run : expandAbbreviation } ] ) ,
362406 ] ,
363407 } ) ;
364408
@@ -629,7 +673,10 @@ async function EditorManager($header, $body) {
629673 // Helper: apply a file's content and language to the editor view
630674 function applyFileToEditor ( file ) {
631675 if ( ! file || file . type !== "editor" ) return ;
676+ const syntax = getEmmetSyntaxForFile ( file ) ;
632677 const baseExtensions = [
678+ // Emmet needs to precede default keymaps so tracker Tab wins over indent
679+ ...createEmmetExtensionSet ( { syntax } ) ,
633680 ...createBaseExtensions ( ) ,
634681 getCommandKeymapExtension ( ) ,
635682 // keep compartment in the state to allow dynamic theme changes later
@@ -640,6 +687,7 @@ async function EditorManager($header, $body) {
640687 ...getBaseExtensionsFromOptions ( ) ,
641688 ] ;
642689 const exts = [ ...baseExtensions ] ;
690+ maybeAttachEmmetCompletions ( exts , syntax ) ;
643691 try {
644692 const langExtFn = file . currentLanguageExtension ;
645693 let initialLang = [ ] ;
@@ -674,13 +722,6 @@ async function EditorManager($header, $body) {
674722 // ignore language extension errors; fallback to plain text
675723 }
676724
677- // Emmet config: set syntax based on file/mode
678- const syntax = getEmmetSyntaxForFile ( file ) ;
679- exts . push ( abbreviationTracker ( ) ) ;
680- exts . push ( wrapWithAbbreviation ( ) ) ;
681- exts . push ( keymap . of ( [ { key : "Mod-e" , run : expandAbbreviation } ] ) ) ;
682- exts . push ( emmetConfig . of ( { syntax } ) ) ;
683-
684725 // Color preview plugin when enabled
685726 if ( appSettings . value . colorPreview ) {
686727 exts . push ( colorView ( true ) ) ;
@@ -742,21 +783,37 @@ async function EditorManager($header, $body) {
742783 const mode = ( file ?. currentMode || "" ) . toLowerCase ( ) ;
743784 const name = ( file ?. filename || "" ) . toLowerCase ( ) ;
744785 const ext = name . includes ( "." ) ? name . split ( "." ) . pop ( ) : "" ;
745- if ( ext === "xml" || mode . includes ( "xml" ) ) return "xml" ;
746- if ( ext === "jsx" || ext === "tsx" ) return "jsx" ;
747- if ( mode . includes ( "javascript" ) && ( ext === "jsx" || ext === "tsx" ) )
748- return "jsx" ;
749- if ( ext === "css" || mode . includes ( "css" ) ) return "css" ;
750- if ( ext === "scss" || mode . includes ( "scss" ) ) return "scss" ;
751- if ( ext === "sass" || mode . includes ( "sass" ) ) return "sass" ;
786+ if ( ext === "tsx" || mode . includes ( "tsx" ) ) return EmmetKnownSyntax . tsx ;
787+ if ( ext === "jsx" || mode . includes ( "jsx" ) ) return EmmetKnownSyntax . jsx ;
788+ if ( mode . includes ( "javascript" ) && ( ext === "jsx" || ext === "tsx" ) ) {
789+ return ext === "tsx" ? EmmetKnownSyntax . tsx : EmmetKnownSyntax . jsx ;
790+ }
791+ if ( ext === "css" || mode . includes ( "css" ) ) return EmmetKnownSyntax . css ;
792+ if ( ext === "scss" || mode . includes ( "scss" ) ) return EmmetKnownSyntax . scss ;
793+ if ( ext === "sass" || mode . includes ( "sass" ) ) return EmmetKnownSyntax . sass ;
794+ if ( ext === "less" || mode . includes ( "less" ) ) return EmmetKnownSyntax . less ;
795+ if ( ext === "sss" || mode . includes ( "sss" ) ) return EmmetKnownSyntax . sss ;
752796 if ( ext === "styl" || ext === "stylus" || mode . includes ( "styl" ) )
753- return "stylus" ;
754- if ( ext === "php" || mode . includes ( "php" ) ) return "html" ; // treat PHP as HTML for Emmet
755- if ( ext === "vue" || mode . includes ( "vue" ) ) return "html" ; // Emmet inside templates
797+ return EmmetKnownSyntax . stylus ;
798+ if ( ext === "postcss" || mode . includes ( "postcss" ) )
799+ return EmmetKnownSyntax . postcss ;
800+ if ( ext === "xml" || mode . includes ( "xml" ) ) return EmmetKnownSyntax . xml ;
801+ if ( ext === "xsl" || mode . includes ( "xsl" ) ) return EmmetKnownSyntax . xsl ;
802+ if ( ext === "haml" || mode . includes ( "haml" ) ) return EmmetKnownSyntax . haml ;
803+ if (
804+ ext === "pug" ||
805+ ext === "jade" ||
806+ mode . includes ( "pug" ) ||
807+ mode . includes ( "jade" )
808+ )
809+ return EmmetKnownSyntax . pug ;
810+ if ( ext === "slim" || mode . includes ( "slim" ) ) return EmmetKnownSyntax . slim ;
811+ if ( ext === "vue" || mode . includes ( "vue" ) ) return EmmetKnownSyntax . vue ;
812+ if ( ext === "php" || mode . includes ( "php" ) ) return EmmetKnownSyntax . html ;
756813 if ( ext === "html" || ext === "xhtml" || mode . includes ( "html" ) )
757- return " html" ;
814+ return EmmetKnownSyntax . html ;
758815 // Defaults to html per Emmet docs
759- return " html" ;
816+ return EmmetKnownSyntax . html ;
760817 }
761818
762819 const $vScrollbar = ScrollBar ( {
0 commit comments