microsoft / monaco-editor

A browser based code editor
https://microsoft.github.io/monaco-editor/
MIT License
39.64k stars 3.54k forks source link

Auto complete for HTML -- add the close tag #221

Open zuznow opened 7 years ago

zuznow commented 7 years ago

Hi There is a way to configure HTML autocomplete I would like that it will add the close tag as done in other editors Thanks Orly

monaco-editor npm version: 0.5.3 Browser: N/A OS: N/A

LoganDark commented 6 years ago

Status?

zhump commented 5 years ago

Status?

LoganDark commented 5 years ago

Only every other editor supports this, I guess it's not important. </sarcasm>

aeschli commented 5 years ago

In VSCode we wait with adding the closing tag until you type the > . That's something to port to the Monaco editor. PRs welcome...

mofux commented 5 years ago

I guess tagClosing.ts needs to be ported to monaco-html

aeschli commented 5 years ago

@mofux Yes, exactly.

christianmcfarland commented 5 years ago

Status?

TortoiseMaster commented 4 years ago

Could be something like this:


editor.onKeyDown(function(e) { 
  if(e.keyCode == 84 && editor.getModel()._languageIdentifier.language == "xml"){
    var position = editor.getPosition();
    var text = editor.getValue(position);
    var splitedText= text.split("\n");
    var line = splitedText[position.lineNumber-1];
    var regex = /<(\w+)[^\/>]*$/;
    if (line.match(regex)) {
      var content = "</"+line.match(regex)[1]+">";
      editor.trigger('bla', 'type', { text: content});
        editor.setPosition(position);
      }
    }
  }, 'editorTextFocus && !suggestWidgetVisible && !renameInputVisible && !inSnippetMode && !quickFixWidgetVisible')
LoganDark commented 4 years ago

@TortoiseMaster regex for parsing XML...

aloksharma1 commented 4 years ago

wow 2020 and still this issue not solved??

LoganDark commented 4 years ago

wow 2020 and still this issue not solved??

Yes, most of the people working on this project are interested in more important issues

aloksharma1 commented 4 years ago

if they ever complete it (which i highly doubt now :P), they better make the tab push in it so i can get a general syntax autocomplete after tab.

LoganDark commented 4 years ago

Seems none of the maintainers really care about bringing Monaco up to date with other editors...

aloksharma1 commented 4 years ago

@TortoiseMaster any progress with your code? i noticed that even style & script syntax are missing when using html language mode.. now i have to think whether to add them manually going through docs or switch to any better alternative sigh.

TortoiseMaster commented 4 years ago

@aloksharma1 Now it looks like this:

function onXMLKeyDownListener(editor)
{
    function isBracketClose(event) {
        return (event.browserEvent && event.browserEvent.key == ">") || (event.keyCode == 84 && e.shiftKey);
    }
    return function(e)
    {
        // console.log("EVENT ", e, isQuote(e), isBracketClose(e))

        if (isBracketClose(e) /*  > */){
            const position = editor.getPosition();
            const text = editor.getValue(position);
            const splitedText= text.split("\n");
            const line = splitedText[position.lineNumber-1];

            let preLine = line.substring(0, position.column-1);
            let postLine = line.substring(position.column-1);

            let i = 1;
            while (preLine.indexOf("<") == -1 && position.lineNumber - i >= 0) {
                preLine = splitedText[position.lineNumber - 1 - i] + preLine;
                i++;
            }

            const regex = /<(\w+)[^\/>]*$/;
            if (preLine.match(regex) && !(postLine.indexOf(">") < postLine.indexOf("<"))) {
                let content = "</"+preLine.match(regex)[1]+">";
                editor.trigger('bla', 'type', {text: content});
                editor.setPosition(position);
            }
        }
    }

}
EmiyaYang commented 3 years ago

My solution.

import monaco from 'monaco-editor/esm/vs/editor/editor.api';

monaco.languages.registerCompletionItemProvider('html', {
  triggerCharacters: ['>'],
  provideCompletionItems: (model, position) => {
    const codePre: string = model.getValueInRange({
      startLineNumber: position.lineNumber,
      startColumn: 1,
      endLineNumber: position.lineNumber,
      endColumn: position.column,
    });

    const tag = codePre.match(/.*<(\w+)>$/)?.[1];

    if (!tag) {
      return {};
    }

    const word = model.getWordUntilPosition(position);

    return {
      suggestions: [
        {
          label: `</${tag}>`,
          kind: monaco.languages.CompletionItemKind.EnumMember,
          insertText: `</${tag}>`,
          range:  {
             startLineNumber: position.lineNumber,
             endLineNumber: position.lineNumber,
             startColumn: word.startColumn,
             endColumn: word.endColumn,
          },
        },
      ],
    };
  },
});
theoomoregbee commented 3 years ago

My solution.

import monaco from 'monaco-editor/esm/vs/editor/editor.api';

monaco.languages.registerCompletionItemProvider('html', {
  triggerCharacters: ['>'],
  provideCompletionItems: (model, position) => {
    const codePre: string = model.getValueInRange({
      startLineNumber: position.lineNumber,
      startColumn: 1,
      endLineNumber: position.lineNumber,
      endColumn: position.column,
    });

    const tag = codePre.match(/.*<(\w+)>$/)?.[1];

    if (!tag) {
      return {};
    }

    const word = model.getWordUntilPosition(position);

    return {
      suggestions: [
        {
          label: `</${tag}>`,
          kind: monaco.languages.CompletionItemKind.EnumMember,
          insertText: `</${tag}>`,
          range:  {
             startLineNumber: position.lineNumber,
             endLineNumber: position.lineNumber,
             startColumn: word.startColumn,
             endColumn: word.endColumn,
          },
        },
      ],
    };
  },
});

Thanks, @EmiyaYang. I was getting some errors on my console, when I'm just typing on the editor without starting with <


errors.ts:22 Uncaught Error: i.suggestions is not iterable

TypeError: i.suggestions is not iterable
    at b (suggest.ts:190)
    at Object.<anonymous> (suggest.ts:238)
    at Generator.next (<anonymous>)
    at r (compressedObjectTreeModel.ts:457)
    at errors.ts:22

So, I replace return {} with just return in the if word doesn't match tag regexp.


...
    const tag = codePre.match(/.*<(\w+)>$/)?.[1];

    if (!tag) {
-       return {};
+       return
    }

    const word = model.getWordUntilPosition(position);
...
flokol120 commented 2 years ago

Here is my solution, which feels a lot like vscode (which is inspired by @EmiyaYang):

const disposable =
  model &&
  model.onDidChangeContent((e) => {
      !e.isRedoing &&
          !e.isUndoing &&
          e.changes
              .filter(({ text }) => text.lastIndexOf(">") !== -1 && text.lastIndexOf(">") === text.length - 1)
              .forEach(({ range, rangeLength }) => {
                  const untilLine = model.getValueInRange({
                      startLineNumber: 1,
                      startColumn: 1,
                      endLineNumber: range.endLineNumber,
                      endColumn: range.endColumn + rangeLength + 1,
                  });

                  // look for return, arrow function and variable assignment first --> don't complete when using generics
                  const enclosingTag = /(?:return[\S\s]*|\s?=\s?|=>.*)<(\w+)>(?:\s*})?$/.exec(untilLine)?.[1];

                  if (!enclosingTag || enclosingTag.includes("/")) {
                      return;
                  }

                  const newRange = new monaco.Range(
                      range.endLineNumber,
                      range.endColumn + rangeLength + 1,
                      range.endLineNumber,
                      range.endColumn + rangeLength + enclosingTag.length + 1
                  );

                  // save content which will be overridden otherwise
                  const rest = model.getValueInRange(newRange);
                  model.applyEdits([
                      {
                          range: newRange,
                          text: `</${enclosingTag}>${rest}`,
                      },
                  ]);
              });
  });

The solution tries to avoid completing generics. Use disposable && disposable.dispose(); if the listener is not used anymore. You just need the model you want to use it on.

JeroenMBooij commented 2 years ago

I modified @EmiyaYang solution to also move the cursor inside the tags when the suggestion is selected.

`

monaco.languages.registerCompletionItemProvider('html', 
{
    triggerCharacters: ['>'],
    provideCompletionItems: (model, position) => 
    {
      const codePre: string = model.getValueInRange({
        startLineNumber: position.lineNumber,
        startColumn: 1,
        endLineNumber: position.lineNumber,
        endColumn: position.column,
      });

      const tag = codePre.match(/.*<(\w+)>$/)?.[1];

      if (!tag) {
        return;
      }

      const word = model.getWordUntilPosition(position);

      return {
        suggestions: [
          {
            label: `</${tag}>`,
            kind: monaco.languages.CompletionItemKind.EnumMember,
            insertText: `$1</${tag}>`,
            insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
            range:  {
               startLineNumber: position.lineNumber,
               endLineNumber: position.lineNumber,
               startColumn: word.startColumn,
               endColumn: word.endColumn,
            },
          },
        ],
      };
    },
});

`

avigoldman commented 2 years ago

Here is my implementation:

import isSelfClosing from 'is-self-closing';

editor.onKeyDown((event) => {
  // when the user enters '>'
  if (event.browserEvent.key === '>') {
    const model = editor.getModel();
    const currentSelections = editor.getSelections();

    const edits = [];
    const newSelections = [];
    // potentially insert the ending tag at each of the selections
    for (const selection of currentSelections) {
      // shift the selection over by one to account for the new character
      newSelections.push(new Selection(
        selection.selectionStartLineNumber,
        selection.selectionStartColumn + 1,
        selection.endLineNumber,
        selection.endColumn + 1
      ));
      // grab the line before the cursor
      const lineBeforeChange = model.getValueInRange({
        startLineNumber: selection.endLineNumber,
        startColumn: 1,
        endLineNumber: selection.endLineNumber,
        endColumn: selection.endColumn,
      });

      // if ends with a HTML tag we are currently closing
      const tag = lineBeforeChange.match(/.*<([\w-]+)$/)?.[1];
      if (!tag) {
        continue;
      }

      // skip self-closing tags like <br> or <img>
      if (isSelfClosing(tag)) {
        continue;
      }

      // add in the closing tag
      edits.push({
        range: {
          startLineNumber: selection.endLineNumber,
          startColumn: selection.endColumn + 1, // add 1 to offset for the inserting '>' character
          endLineNumber: selection.endLineNumber,
          endColumn: selection.endColumn + 1,
        },
        text: `</${tag}>`,
      });
    }

    // wait for next tick to avoid it being an invalid operation
    setTimeout(() => {
      editor.executeEdits(
        model.getValue(),
        edits,
        newSelections
      );
    }, 0);
  }
})
hatemhosny commented 1 year ago

This is a modification of the code by @avigoldman


  editor.onKeyDown((event) => {
    // select enabled languages
    const enabledLanguages = ["html", "markdown", "javascript", "typescript"]; // enable js & ts for jsx & tsx

    const model = editor.getModel();
    if (!enabledLanguages.includes(model.getLanguageId())) {
      return;
    }

    const isSelfClosing = (tag) =>
      [
        "area",
        "base",
        "br",
        "col",
        "command",
        "embed",
        "hr",
        "img",
        "input",
        "keygen",
        "link",
        "meta",
        "param",
        "source",
        "track",
        "wbr",
        "circle",
        "ellipse",
        "line",
        "path",
        "polygon",
        "polyline",
        "rect",
        "stop",
        "use",
      ].includes(tag);

    // when the user enters '>'
    if (event.browserEvent.key === ">") {
      const currentSelections = editor.getSelections();

      const edits = [];
      const newSelections = [];
      // potentially insert the ending tag at each of the selections
      for (const selection of currentSelections) {
        // shift the selection over by one to account for the new character
        newSelections.push(
          new monaco.Selection(
            selection.selectionStartLineNumber,
            selection.selectionStartColumn + 1,
            selection.endLineNumber,
            selection.endColumn + 1,
          ),
        );
        // grab the content before the cursor
        const contentBeforeChange = model.getValueInRange({
          startLineNumber: 1,
          startColumn: 1,
          endLineNumber: selection.endLineNumber,
          endColumn: selection.endColumn,
        });

        // if ends with a HTML tag we are currently closing
        const match = contentBeforeChange.match(/<([\w-]+)(?![^>]*\/>)[^>]*$/);
        if (!match) {
          continue;
        }

        const [fullMatch, tag] = match;

        // skip self-closing tags like <br> or <img>
        if (isSelfClosing(tag) || fullMatch.trim().endsWith("/")) {
          continue;
        }

        // add in the closing tag
        edits.push({
          range: {
            startLineNumber: selection.endLineNumber,
            startColumn: selection.endColumn + 1, // add 1 to offset for the inserting '>' character
            endLineNumber: selection.endLineNumber,
            endColumn: selection.endColumn + 1,
          },
          text: `</${tag}>`,
        });
      }

      // wait for next tick to avoid it being an invalid operation
      setTimeout(() => {
        editor.executeEdits(model.getValue(), edits, newSelections);
      }, 0);
    }
  });

modifications:

try it in LiveCodes playground

edit: changed regular expression to improve performance

OG0808 commented 2 months ago

This is my solution for TypeScript with Next.js Captura de pantalla 2024-06-05 204319