monaco-editor入门

2,312 阅读10分钟

Monaco Editor 是一款开源的在线代码编辑器,它是 VSCode 的浏览器版本。

Monaco Editor的文档:

20211211155216

Monaco Editor的文档目前并不是很全,主要可以看它的palyground,如下图:

20211211155728

一开始看这个playground也比较懵,两个编辑器?

左边是代码加注释,这就算是文档了,右边就是实现的效果了。

还有一个示例切换的select,开始选择不同的示例:

20211211160043

快速上手

安装:

yarn monaco-editor@0.31.0
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';

// editor.api.js只是基础功能,你可以还需要导入一些文件,比如javascript语法支持文件
import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js';

monaco.editor.create(document.getElementById('container'), {
	value: "function hello() {\n\talert('Hello world!');\n}",
	language: 'javascript'
});

自动补全

文档:

文档很少,可以直接看代码注释,通过typescript查看定义可以很方便的找到,vscode和monaco-editor可以结合起来看,manaco-editor中的api基本都能在vscode中找到对应的,vscode中的注释写的更详细一些,但是它们还是有一些细微的区别的。

provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult<T[] | CompletionList<T>>;

每次输入会触发provideCompletionItems, 主要根据文档内容document和光标位置position得到CompletionListdocument是纯文本内容, 如有需要,可以要自己来做语法解析得到相关信息。

CompletionItemProvider

/**
 * The completion item provider interface defines the contract between extensions and
 * [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense).
 *
 * Providers can delay the computation of the {@linkcode CompletionItem.detail detail}
 * and {@linkcode CompletionItem.documentation documentation} properties by implementing the
 * {@linkcode CompletionItemProvider.resolveCompletionItem resolveCompletionItem}-function. However, properties that
 * are needed for the initial sorting and filtering, like `sortText`, `filterText`, `insertText`, and `range`, must
 * not be changed during resolve.
 *
 * Providers are asked for completions either explicitly by a user gesture or -depending on the configuration-
 * implicitly when typing words or trigger characters.
 */
export interface CompletionItemProvider<T extends CompletionItem = CompletionItem> {

		/**
		 * Provide completion items for the given position and document.
		 *
		 * @param document The document in which the command was invoked.
		 * @param position The position at which the command was invoked.
		 * @param token A cancellation token.
		 * @param context How the completion was triggered.
		 *
		 * @return An array of completions, a {@link CompletionList completion list}, or a thenable that resolves to either.
		 * The lack of a result can be signaled by returning `undefined`, `null`, or an empty array.
		 */
		provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult<T[] | CompletionList<T>>;

		/**
		 * Given a completion item fill in more data, like {@link CompletionItem.documentation doc-comment}
		 * or {@link CompletionItem.detail details}.
		 *
		 * The editor will only resolve a completion item once.
		 *
		 * *Note* that this function is called when completion items are already showing in the UI or when an item has been
		 * selected for insertion. Because of that, no property that changes the presentation (label, sorting, filtering etc)
		 * or the (primary) insert behaviour ({@link CompletionItem.insertText insertText}) can be changed.
		 *
		 * This function may fill in {@link CompletionItem.additionalTextEdits additionalTextEdits}. However, that means an item might be
		 * inserted *before* resolving is done and in that case the editor will do a best effort to still apply those additional
		 * text edits.
		 *
		 * @param item A completion item currently active in the UI.
		 * @param token A cancellation token.
		 * @return The resolved completion item or a thenable that resolves to of such. It is OK to return the given
		 * `item`. When no result is returned, the given `item` will be used.
		 */
		resolveCompletionItem?(item: T, token: CancellationToken): ProviderResult<T>;
}

TextDocument

表示一个文本文档,例如源文件。TextDocument包含 {@link TextLine lines} 和 底层文件的一些信息,比如文件路径uri

/**
 * Represents a text document, such as a source file. Text documents have
 * {@link TextLine lines} and knowledge about an underlying resource like a file.
 */
export interface TextDocument {

    /**
     * The associated uri for this document.
     *
     * *Note* that most documents use the `file`-scheme, which means they are files on disk. However, **not** all documents are
     * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk.
     *
     * @see {@link FileSystemProvider}
     * @see {@link TextDocumentContentProvider}
     */

		/**
		 * document的关联uri。
		 * 注意: 多数document使用`file`属性,这意味着它们是磁盘上的文件。然而,并不是所有的document都是保存在磁盘上的。
		 * 因此在尝试访问底层文件之前,必须检查这个属性。
		 *  
		*/
    readonly uri: Uri;

    /**
     * The file system path of the associated resource. Shorthand
     * notation for {@link TextDocument.uri TextDocument.uri.fsPath}. Independent of the uri scheme.
     */
		/**
		 * 关联的文件系统路径。{@link TextDocument.uri TextDocument.uri.fsPath}的速记符。
		*/
    readonly fileName: string;

    /**
     * Is this document representing an untitled file which has never been saved yet. *Note* that
     * this does not mean the document will be saved to disk, use {@linkcode Uri.scheme}
     * to figure out where a document will be {@link FileSystemProvider saved}, e.g. `file`, `ftp` etc.
     */
		/**
		 * 表示这是还没有被保存一个无标题的文件,*注意*: 这并不意味着document将被保存到磁盘,
		 * 使用{@linkcode Uri.scheme}来决定document将要被保存在何处,例如, `file`, `ftp`等。
		*/
    readonly isUntitled: boolean;

    /**
     * The identifier of the language associated with this document.
     */
		/**
		 * language标识
		*/
    readonly languageId: string;

    /**
     * The version number of this document (it will strictly increase after each
     * change, including undo/redo).
     */
		/**
		 * document的版本(它将在每次更改后严格增加,包括撤消/重做)
		*/
    readonly version: number;

    /**
     * `true` if there are unpersisted changes.
     */
		/**
		 * 如果修改了未保存则为`true`。
		*/
    readonly isDirty: boolean;

    /**
     * `true` if the document has been closed. A closed document isn't synchronized anymore
     * and won't be re-used when the same resource is opened again.
     */
		/**
		 * 如果document已经关闭,则为`true`。一个关闭的document将不再同步,
		 * 并且在同一资源再次打开时不会被重用。
		*/
    readonly isClosed: boolean;

    /**
     * Save the underlying file.
     *
     * @return A promise that will resolve to true when the file
     * has been saved. If the file was not dirty or the save failed,
     * will return false.
     */
		/**
		 * 保存底层文件。
		 * 返回一个promise,如果promise resolve为`true`,表示文件保存成功。
		 * 如果为`false`,则文件未修改或者保存失败。
		*/
    save(): Thenable<boolean>;

    /**
     * The {@link EndOfLine end of line} sequence that is predominately
     * used in this document.
     */
		/**
		 * 此document使用的endOfLine。
		*/
    readonly eol: EndOfLine;

    /**
     * The number of lines in this document.
     */
		/**
		 * 此document的行数。
		*/
    readonly lineCount: number;

    /**
     * Returns a text line denoted by the line number. Note
     * that the returned object is *not* live and changes to the
     * document are not reflected.
     *
     * @param line A line number in [0, lineCount).
     * @return A {@link TextLine line}.
     */

		/**
		 * 返回某行的text,一个TextLine对象。
		 * 返回的对象是*非*活动的,对document的更改不会反映出来。
		*/
    lineAt(line: number): TextLine;

    /**
     * Returns a text line denoted by the position. Note
     * that the returned object is *not* live and changes to the
     * document are not reflected.
     *
     * The position will be {@link TextDocument.validatePosition adjusted}.
     *
     * @see {@link TextDocument.lineAt}
     *
     * @param position A position.
     * @return A {@link TextLine line}.
     */
    lineAt(position: Position): TextLine;

    /**
     * Converts the position to a zero-based offset.
     *
     * The position will be {@link TextDocument.validatePosition adjusted}.
     *
     * @param position A position.
     * @return A valid zero-based offset.
     */
		/**
		 * 将position对象转换为基于零的偏移量。
		*/
    offsetAt(position: Position): number;

    /**
     * Converts a zero-based offset to a position.
     *
     * @param offset A zero-based offset.
     * @return A valid {@link Position}.
     */
    positionAt(offset: number): Position;

    /**
     * Get the text of this document. A substring can be retrieved by providing
     * a range. The range will be {@link TextDocument.validateRange adjusted}.
     *
     * @param range Include only the text included by the range.
     * @return The text inside the provided range or the entire text.
     */
    getText(range?: Range): string;

    /**
     * Get a word-range at the given position. By default words are defined by
     * common separators, like space, -, _, etc. In addition, per language custom
     * [word definitions} can be defined. It
     * is also possible to provide a custom regular expression.
     *
     * * *Note 1:* A custom regular expression must not match the empty string and
     * if it does, it will be ignored.
     * * *Note 2:* A custom regular expression will fail to match multiline strings
     * and in the name of speed regular expressions should not match words with
     * spaces. Use {@linkcode TextLine.text} for more complex, non-wordy, scenarios.
     *
     * The position will be {@link TextDocument.validatePosition adjusted}.
     *
     * @param position A position.
     * @param regex Optional regular expression that describes what a word is.
     * @return A range spanning a word, or `undefined`.
     */
		/**
		 * 给定一个position,通常是光标的位置,得到光标所在单词的range。
		 * 默认单词使用空格,-,_等分隔,每个language可以自定义([word definitions})。
		*/
    getWordRangeAtPosition(position: Position, regex?: RegExp): Range | undefined;

    /**
     * Ensure a range is completely contained in this document.
     *
     * @param range A range.
     * @return The given range or a new, adjusted range.
     */
		/**
		 * 确保range完全包含在document之中,不会超出范围。
		 * 返回传入的range或经过调整的新的range。
		*/
    validateRange(range: Range): Range;

    /**
     * Ensure a position is contained in the range of this document.
     *
     * @param position A position.
     * @return The given position or a new, adjusted position.
     */
		/**
		 * 确保position在本文档的范围内。
		*/
    validatePosition(position: Position): Position;
}

Position

表示一行或者一个字符的位置,比如说光标的位置; line表示行,character表示列,索引从0开始计数,{ line: 2, character: 5 }表示第3行第6列。

Postion 对象是不可变的,可以使用withtranslate方法根据存在的position生成新的position

/**
 * Represents a line and character position, such as
 * the position of the cursor.
 *
 * Position objects are __immutable__. Use the {@link Position.with with} or
 * {@link Position.translate translate} methods to derive new positions
 * from an existing position.
 */
export class Position {

    /**
     * The zero-based line value.
     */
    readonly line: number;

    /**
     * The zero-based character value.
     */
    readonly character: number;

    /**
     * @param line A zero-based line value.
     * @param character A zero-based character value.
     */
    constructor(line: number, character: number);

    /**
     * Check if this position is before `other`.
     *
     * @param other A position.
     * @return `true` if position is on a smaller line
     * or on the same line on a smaller character.
     */
    isBefore(other: Position): boolean;

    /**
     * Check if this position is before or equal to `other`.
     *
     * @param other A position.
     * @return `true` if position is on a smaller line
     * or on the same line on a smaller or equal character.
     */
    isBeforeOrEqual(other: Position): boolean;

    /**
     * Check if this position is after `other`.
     *
     * @param other A position.
     * @return `true` if position is on a greater line
     * or on the same line on a greater character.
     */
    isAfter(other: Position): boolean;

    /**
     * Check if this position is after or equal to `other`.
     *
     * @param other A position.
     * @return `true` if position is on a greater line
     * or on the same line on a greater or equal character.
     */
    isAfterOrEqual(other: Position): boolean;

    /**
     * Check if this position is equal to `other`.
     *
     * @param other A position.
     * @return `true` if the line and character of the given position are equal to
     * the line and character of this position.
     */
    isEqual(other: Position): boolean;

    /**
     * Compare this to `other`.
     *
     * @param other A position.
     * @return A number smaller than zero if this position is before the given position,
     * a number greater than zero if this position is after the given position, or zero when
     * this and the given position are equal.
     */
    compareTo(other: Position): number;

    /**
     * Create a new position relative to this position.
     *
     * @param lineDelta Delta value for the line value, default is `0`.
     * @param characterDelta Delta value for the character value, default is `0`.
     * @return A position which line and character is the sum of the current line and
     * character and the corresponding deltas.
     */
    translate(lineDelta?: number, characterDelta?: number): Position;

    /**
     * Derived a new position relative to this position.
     *
     * @param change An object that describes a delta to this position.
     * @return A position that reflects the given delta. Will return `this` position if the change
     * is not changing anything.
     */
    translate(change: { lineDelta?: number; characterDelta?: number; }): Position;

    /**
     * Create a new position derived from this position.
     *
     * @param line Value that should be used as line value, default is the {@link Position.line existing value}
     * @param character Value that should be used as character value, default is the {@link Position.character existing value}
     * @return A position where line and character are replaced by the given values.
     */
    with(line?: number, character?: number): Position;

    /**
     * Derived a new position from this position.
     *
     * @param change An object that describes a change to this position.
     * @return A position that reflects the given change. Will return `this` position if the change
     * is not changing anything.
     */
    with(change: { line?: number; character?: number; }): Position;
}

Range

通过两个position指定的一个范围。{ start, end }

TextLine

表示一行文本。

/**
 * Represents a line of text, such as a line of source code.
 *
 * TextLine objects are __immutable__. When a {@link TextDocument document} changes,
 * previously retrieved lines will not represent the latest state.
 */
export interface TextLine {

    /**
     * The zero-based line number.
     */
    readonly lineNumber: number;

    /**
     * The text of this line without the line separator characters.
     */
    readonly text: string;

    /**
     * The range this line covers without the line separator characters.
     */
    readonly range: Range;

    /**
     * The range this line covers with the line separator characters.
     */
    readonly rangeIncludingLineBreak: Range;

    /**
     * The offset of the first character which is not a whitespace character as defined
     * by `/\s/`. **Note** that if a line is all whitespace the length of the line is returned.
     */
		/**
		 * 第一个不为Whitespace的字符的index。
		*/
    readonly firstNonWhitespaceCharacterIndex: number;

    /**
     * Whether this line is whitespace only, shorthand
     * for {@link TextLine.firstNonWhitespaceCharacterIndex} === {@link TextLine.text TextLine.text.length}.
     */
		/**
		 * 这一行是否仅为空格
		*/
    readonly isEmptyOrWhitespace: boolean;
}

参考项目

(vscode-docker)[github.com/microsoft/v…], 编写Dockfile的语法比较简单,适合拿来做参考。

主要涉及3个项目:

vscode-docker依赖dockerfile-language-server-nodejsdockerfile-language-server-nodejs依赖dockerfile-language-service

dockerfile-language-server-nodejs只包含启动符合LSPDockerfile language server所需的代码。解析Dockerfile和提供编辑器特性(如代码完成或悬浮)的实际代码在dockerfile-language-service

所以我们可以直接看dockerfile-language-service的代码就好了。

我以一个例子来说明:

20211216112046

当检测到输入满足COPY --from=的时候,给出自动补全提示,包括为在文件开头定义的两个image。 这是一个很好的例子,因为它包含语义分析的结果。

如何跟踪dockerfile-language-service代码?

跟踪dockerfile-language-service代码

根据dockerfile-language-service的单元测试,我找到一种比较方便的方式。

  1. dockerfile-language-service源码中新建dockerfile-language-service/temp/main.ts文件:
import {
   Position, CompletionItem
} from 'vscode-languageserver-types';

import {  DockerfileLanguageServiceFactory } from '../src/main';

const service = DockerfileLanguageServiceFactory.createLanguageService();


function computePosition(content: string, line: number, character: number, snippetSupport?: boolean): CompletionItem[] {
  if (snippetSupport === undefined) {
      snippetSupport = true;
  }
  service.setCapabilities({ completion: { completionItem: { snippetSupport: snippetSupport } }});
  let items = service.computeCompletionItems(content, Position.create(line, character));
  return items as CompletionItem[];
}

const text = `From node:latest as node
From nginx:latest as nginx

COPY --from=`;

// 光标位置,第4行第13列
var proposals = computePosition(text, 3, 12);

console.log(proposals);

  1. 在vscode中配置调整main.ts

.vscode/launch.json:

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/temp/main.ts",
      "preLaunchTask": "tsc:build", // 要在调试会话开始之前执行的任务,任务在`.vscode/tasks.json`定义,这里的作用是编译ts文件。
      "outFiles": ["${workspaceFolder}/out/**/*.js"]
    }
  ]
}

.vscode/tasks.json:

{
	"version": "2.0.0",
	"tasks": [
		{
      "type": "shell",
      "label": "tsc:build",
      "command": "${workspaceFolder}/node_modules/.bin/tsc",
      "args": ["-p", "."],
      "problemMatcher": "$tsc"
    }
	]
}

这样操作之后,我们就可以很方便的调试dockerfile-language-service 的代码了。