blob: ec04c3c3ae88a65ae075d27ab4cd6e2fb485299f [file] [log] [blame]
// Copyright (c) 2014, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
library codemirror;
import 'dart:async';
import 'dart:html';
import 'dart:js';
import 'src/js_utils.dart';
// TODO: find, replace
/// A parameter type into the [CodeMirror.addCommand] method.
typedef CommandHandler = void Function(CodeMirror editor);
/// A parameter type into the [Doc.eachLine] method.
typedef LineHandler = void Function(LineHandle line);
/// A parameter type into the [Doc.extendSelectionsBy] method.
typedef SelectionExtender = Position Function(Span range, int i);
/// A wrapper around the CodeMirror editor.
class CodeMirror extends ProxyHolder {
static final List<String> THEMES = const [
'3024-day',
'3024-night',
'abcdef',
'ambiance-mobile',
'ambiance',
'base16-dark',
'base16-light',
'blackboard',
'cobalt',
'colorforth',
'darcula',
'dracula',
'duotone-dark',
'duotone-light',
'eclipse',
'elegant',
'erlang-dark',
'gruvbox-dark',
'hopscotch',
'icecoder',
'idea',
'isotope',
'lesser-dark',
'liquibyte',
'lucario',
'material',
'mbo',
'mdn-like',
'midnight',
'monokai',
'neat',
'neo',
'night',
'nord',
'oceanic-next',
'panda-syntax',
'paraiso-dark',
'paraiso-light',
'pastel-on-dark',
'railscasts',
'rubyblue',
'seti',
'shadowfox',
'solarized',
'ssms',
'the-matrix',
'tomorrow-night-bright',
'tomorrow-night-eighties',
'ttcn',
'twilight',
'vibrant-ink',
'xq-dark',
'xq-light',
'zenburn',
'yonce',
'yeti',
];
static final List<String> KEY_MAPS = const [
'default',
'emacs',
'sublime',
'vim',
];
static JsObject get _cm => context['CodeMirror'];
static Map<JsObject, CodeMirror> _instances = {};
static List<String> get MODES =>
List.from(keys(_cm['modes']).where((modeName) => modeName != 'null'));
static List<String> get MIME_MODES => List.from(keys(_cm['mimeModes']));
static List<String> get COMMANDS => List.from(keys(_cm['commands']));
/// It contains a string that indicates the version of the library. This is a
/// triple of integers "major.minor.patch", where patch is zero for releases,
/// and something else (usually one) for dev snapshots.
static String get version => _cm['version'];
static ModeInfo findModeByExtension(String ext) =>
ModeInfo(_cm.callMethod('findModeByExtension', [ext]));
static ModeInfo findModeByMime(String mime) =>
ModeInfo(_cm.callMethod('findModeByMIME', [mime]));
static ModeInfo findModeByFileName(String name) =>
ModeInfo(_cm.callMethod('findModeByFileName', [name]));
static ModeInfo findModeByName(String name) =>
ModeInfo(_cm.callMethod('findModeByName', [name]));
/// If you want to define extra methods in terms of the CodeMirror API, it is
/// possible to use defineExtension. This will cause the given value (usually a
/// method) to be added to all CodeMirror instances created from then on.
static void defineExtension(String name, dynamic value) {
_cm.callMethod('defineExtension', [name, value]);
}
/// Like defineExtension, but the method will be added to the interface for Doc
/// objects instead.
static void defineDocExtension(String name, dynamic value) {
_cm.callMethod('defineDocExtension', [name, value]);
}
/// Registers a helper value with the given name in the given namespace (type).
/// This is used to define functionality that may be looked up by mode. Will
/// create (if it doesn't already exist) a property on the CodeMirror object
/// for the given type, pointing to an object that maps names to values. I.e.
/// after doing CodeMirror.registerHelper("hint", "foo", myFoo), the value
/// CodeMirror.hint.foo will point to myFoo.
static void registerHelper(String type, String mode, dynamic helper) {
_cm.callMethod('registerHelper', [type, mode, helper]);
}
// ///
// /// Acts like registerHelper, but also registers this helper as 'global',
// /// meaning that it will be included by getHelpers whenever the given
// /// predicate returns true when called with the local mode and editor.
//
// static void registerGlobalHelper(String type, String mode,
// Function predicate, dynamic helper) {
// // predicate: fn(mode, CodeMirror)
// // TODO: value may be a Function? always a function?
// }
static JsObject _createFromElement(Element element, Map options) {
if (options == null) {
return JsObject(_cm, [element]);
} else {
return JsObject(_cm, [element, jsify(options)]);
}
}
static JsObject _createFromTextArea(TextAreaElement textArea, Map options) {
List args = <dynamic>[textArea];
if (options != null) args.add(jsify(options));
return _cm.callMethod('fromTextArea', args);
}
/// Add a new custom command to CodeMirror.
static void addCommand(String name, CommandHandler callback) {
_cm['commands'][name] = (JsObject obj) {
var editor = CodeMirror.fromJsObject(obj);
callback(editor);
};
}
Doc _doc;
/// Create a new CodeMirror editor in the given element. See
/// http://codemirror.net/doc/manual.html#config for valid options values.
CodeMirror.fromElement(Element element, {Map options})
: super(_createFromElement(element, options)) {
_instances[jsProxy] = this;
}
/// Create a new CodeMirror editor from the given JsObject. This will return an
/// existing Dart `CodeMirror` object if there is already one for the given
/// JavaScript proxy.
factory CodeMirror.fromJsObject(JsObject object) {
if (_instances.containsKey(object)) {
return _instances[object];
} else {
return CodeMirror._fromJsObject(object);
}
}
CodeMirror._fromJsObject(JsObject object) : super(object) {
_instances[jsProxy] = this;
}
/// The method provides another way to initialize an editor. It takes a
/// textarea DOM node as first argument and an optional configuration object as
/// second. It will replace the textarea with a CodeMirror instance, and wire
/// up the form of that textarea (if any) to make sure the editor contents are
/// put into the textarea when the form is submitted. The text in the textarea
/// will provide the content for the editor. A CodeMirror instance created this
/// way has three additional methods: `save`, `toTextArea`, and `getTextArea`.
CodeMirror.fromTextArea(TextAreaElement textArea, {Map options})
: super(_createFromTextArea(textArea, options)) {
_instances[jsProxy] = this;
}
/// Fires every time the content of the editor is changed.
Stream get onChange => onEvent('change', argCount: 2);
/// Will be fired when the cursor or selection moves, or any change is made to
/// the editor content.
Stream get onCursorActivity => onEvent('cursorActivity');
/// Fired when a mouse is clicked. You can preventDefault the event to signal
/// that CodeMirror should do no further handling.
Stream<MouseEvent> get onMouseDown =>
onEvent('mousedown', argCount: 2).cast<MouseEvent>();
/// Fired when a mouse is double-clicked. You can preventDefault the event to
/// signal that CodeMirror should do no further handling.
Stream<MouseEvent> get onDoubleClick =>
onEvent('dblclick', argCount: 2).cast<MouseEvent>();
/// Fires when the editor gutter (the line-number area) is clicked.
Stream<int> get onGutterClick => onEvent<int>('gutterClick', argCount: 4);
/// Retrieve the currently active document from an editor.
Doc getDoc() {
if (_doc == null) {
_doc = Doc.fromProxy(call('getDoc'));
}
return _doc;
}
/// Attach a new document to the editor.
void swapDoc(Doc doc) {
_doc = doc;
callArg('swapDoc', doc.jsProxy);
}
/// Retrieves the current value of the given option for this editor instance.
dynamic getOption(String option) => callArg('getOption', option);
/// Change the configuration of the editor. [option] should the name of an
/// option, and value should be a valid value for that option.
void setOption(String option, dynamic value) =>
callArgs('setOption', [option, value]);
String getTheme() => getOption('theme');
void setTheme(String theme) => setOption('theme', theme);
String getMode() => getOption('mode');
void setMode(String mode) => setOption('mode', mode);
/// Return the current key map.
String getKeyMap() => getOption('keyMap');
/// Valid options are `default`, `vim`, `emacs`, and `sublime`.
void setKeyMap(String value) => setOption('keyMap', value);
/// Whether to show line numbers to the left of the editor.
bool getLineNumbers() => getOption('lineNumbers');
/// Whether to show line numbers to the left of the editor.
void setLineNumbers(bool value) => setOption('lineNumbers', value);
/// Get the content of line n.
String getLine(int n) => callArg('getLine', n);
/// Whether, when indenting, the first N*tabSize spaces should be replaced by N
/// tabs. Default is false.
bool getIndentWithTabs() => getOption('indentWithTabs');
/// Whether, when indenting, the first N*tabSize spaces should be replaced by N
/// tabs.
void setIndentWithTabs(bool value) => setOption('indentWithTabs', value);
/// Whether editing is disabled.
bool getReadOnly() =>
getOption('readOnly') == true ||
getOption('readOnly') == 'true' ||
getOption('readOnly') == 'nocursor';
/// This disables editing of the editor content by the user.
void setReadOnly(bool value, [bool noCursor = false]) {
if (value) {
if (noCursor) {
setOption('readOnly', 'nocursor');
} else {
setOption('readOnly', value);
}
} else {
setOption('readOnly', value);
}
}
/// The width of a tab character. Defaults to 4.
int getTabSize() => getOption('tabSize');
/// The width of a tab character.
void setTabSize(int value) => setOption('tabSize', value);
/// How many spaces a block (whatever that means in the edited language) should
/// be indented. The default is 2.
int getIndentUnit() => getOption('indentUnit');
/// How many spaces a block (whatever that means in the edited language) should
/// be indented.
void setIndentUnit(int value) => setOption('indentUnit', value);
/// If your code does something to change the size of the editor element
/// (window resizes are already listened for), or unhides it, you should
/// probably follow up by calling this method to ensure CodeMirror is still
/// looking as intended.
void refresh() => call('refresh');
/// Give the editor focus.
void focus() => call('focus');
/// Returns the input field for the editor. Will be a textarea or an editable
/// div, depending on the value of the inputStyle option.
Element getInputField() => call('getInputField');
/// Retrieve one end of the primary selection. [start] is a an optional string
/// indicating which end of the selection to return. It may be "from", "to",
/// "head" (the side of the selection that moves when you press shift+arrow),
/// or "anchor" (the fixed side of the selection). Omitting the argument is the
/// same as passing "head". A {line, ch} object will be returned.
Position getCursor([String start]) => Position.fromProxy(
start == null ? call('getCursor') : callArg('getCursor', start));
/// Runs the command with the given name on the editor.
void execCommand(String name) {
callArg('execCommand', name);
}
/// Sets the gutter marker for the given gutter (identified by its CSS class,
/// see the gutters option) to the given value. Value can be either null, to
/// clear the marker, or a DOM element, to set it. The DOM element will be
/// shown in the specified gutter next to the specified line.
void setGutterMarker(int line, String gutterID, Element value) {
callArgs('setGutterMarker', [line, gutterID, value]);
}
/// Remove all gutter markers in the gutter with the given ID.
void clearGutter(String gutterID) {
callArg('clearGutter', gutterID);
}
/// Puts node, which should be an absolutely positioned DOM node, into the
/// editor, positioned right below the given {line, ch} position. When
/// scrollIntoView is true, the editor will ensure that the entire node is
/// visible (if possible). To remove the widget again, simply use DOM methods
/// (move it somewhere else, or call removeChild on its parent).
void addWidget(Position pos, Element node, [bool scrollIntoView = false]) {
callArgs('addWidget', [pos.toProxy(), node, scrollIntoView]);
}
/// Adds a line widget, an element shown below a line, spanning the whole of
/// the editor's width, and moving the lines below it downwards. [line] should
/// be either an integer or a [LineHandle], and node should be a DOM node,
/// which will be displayed below the given line.
///
/// [coverGutter]: whether the widget should cover the gutter.
/// [noHScroll]: whether the widget should stay fixed in the face of horizontal
/// scrolling.
/// [above]: causes the widget to be placed above instead of below the text of
/// the line.
/// [handleMouseEvents]: determines whether the editor will capture mouse and
/// drag events occurring in this widget. Default is false — the events will be
/// left alone for the default browser handler, or specific handlers on the
/// widget, to capture.
/// [insertAt]: by default, the widget is added below other widgets for the
/// line. This option can be used to place it at a different position (zero for
/// the top, N to put it after the Nth other widget). Note that this only has
/// effect once, when the widget is created.
LineWidget addLineWidget(dynamic line, Element node,
{bool coverGutter,
bool noHScroll,
bool above,
bool handleMouseEvents,
int insertAt}) {
Map options = {};
if (coverGutter != null) options['coverGutter'] = coverGutter;
if (noHScroll != null) options['noHScroll'] = noHScroll;
if (above != null) options['above'] = above;
if (handleMouseEvents != null) {
options['handleMouseEvents'] = handleMouseEvents;
}
if (insertAt != null) options['insertAt'] = insertAt;
var l = line is LineHandle ? line.jsProxy : line;
return LineWidget(callArgs('addLineWidget', [l, node, jsify(options)]));
}
/// Set a CSS class name for the given line. [line] can be a number or a
/// [LineHandle]. [where] determines to which element this class should be
/// applied, can can be one of "text" (the text element, which lies in front of
/// the selection), "background" (a background element that will be behind the
/// selection), "gutter" (the line's gutter space), or "wrap" (the wrapper node
/// that wraps all of the line's elements, including gutter elements).
/// [cssClass] should be the name of the class to apply.
LineHandle addLineClass(dynamic line, String where, String cssClass) {
var l = line is LineHandle ? line.jsProxy : line;
return LineHandle(callArgs('addLineClass', [l, where, cssClass]));
}
/// Remove a CSS class from a line. [line] can be a [LineHandle] or number.
/// [where] should be one of "text", "background", or "wrap" (see
/// [addLineClass]). [cssClass] can be left off to remove all classes for the
/// specified node, or be a string to remove only a specific class.
LineHandle removeLineClass(dynamic line, String where, [String cssClass]) {
var l = line is LineHandle ? line.jsProxy : line;
if (cssClass == null) {
return LineHandle(callArgs('removeLineClass', [l, where]));
} else {
return LineHandle(callArgs('removeLineClass', [l, where, cssClass]));
}
}
/// Retrieves information about the token the current mode found before the
/// given position.
///
/// If [precise] is true, the token will be guaranteed to be accurate based on
/// recent edits. If false or not specified, the token will use cached state
/// information, which will be faster but might not be accurate if edits were
/// recently made and highlighting has not yet completed.
Token getTokenAt(Position pos, [bool precise]) {
var r = precise == null
? callArg('getTokenAt', pos.toProxy())
: callArgs('getTokenAt', [pos.toProxy(), precise]);
return Token.fromProxy(r);
}
/// This is similar to getTokenAt, but collects all tokens for a given line
/// into an array. It is much cheaper than repeatedly calling getTokenAt,
/// which re-parses the part of the line before the token for every call.
List<Token> getLineTokens(int line, [bool precise]) {
var result = precise != null
? callArgs('getLineTokens', [line, precise])
: callArg('getLineTokens', line);
if (result is List) {
return List.from(result.map((t) => Token.fromProxy(t)));
} else {
return [];
}
}
/// This is a (much) cheaper version of getTokenAt useful for when you just
/// need the type of the token at a given position, and no other information.
/// Will return null for unstyled tokens, and a string, potentially containing
/// multiple space-separated style names, otherwise.
String getTokenTypeAt(Position pos) => callArg('getTokenTypeAt', pos);
/// Programmatically set the size of the editor (overriding the applicable CSS
/// rules). [width] and [height] can be either numbers (interpreted as pixels)
/// or CSS units ("100%", for example). You can pass `null` for either of them
/// to indicate that that dimension should not be changed.
void setSize(num width, num height) => callArgs('setSize', [width, height]);
/// Scroll the editor to a given (pixel) position. Both arguments may be left
/// as null or undefined to have no effect.
void scrollTo(num x, num y) => callArgs('scrollTo', [x, y]);
/// Get a [ScrollInfo] object that represents the current scroll position, the
/// size of the scrollable area, and the size of the visible area (minus
/// scrollbars).
ScrollInfo getScrollInfo() => ScrollInfo(call('getScrollInfo'));
/// Scrolls the given position into view. The margin parameter is optional.
/// When given, it indicates the amount of vertical pixels around the given
/// area that should be made visible as well.
void scrollIntoView(int line, int ch, {int margin}) {
if (margin != null) {
callArgs('scrollIntoView', [
JsObject.jsify({'line': line, 'ch': ch}),
margin,
]);
} else {
callArgs('scrollIntoView', [
JsObject.jsify({'line': line, 'ch': ch}),
]);
}
}
/// Fetch the set of applicable helper values for the given position. Helpers
/// provide a way to look up functionality appropriate for a mode. The type
/// argument provides the helper namespace (see registerHelper), in which the
/// values will be looked up. When the mode itself has a property that
/// corresponds to the type, that directly determines the keys that are used to
/// look up the helper values (it may be either a single string, or an array of
/// strings). Failing that, the mode's helperType property and finally the
/// mode's name are used.
///
/// For example, the JavaScript mode has a property fold containing "brace".
/// When the brace-fold addon is loaded, that defines a helper named brace in
/// the fold namespace. This is then used by the foldcode addon to figure out
/// that it can use that folding function to fold JavaScript code.
///
/// When any 'global' helpers are defined for the given namespace, their
/// predicates are called on the current mode and editor, and all those that
/// declare they are applicable will also be added to the array that is
/// returned.
List<JsObject> getHelpers(Position pos, String type) {
return List.from(callArgs('getHelpers', [pos.toProxy(), type]));
}
/// Returns the first applicable helper value.
JsObject getHelper(Position pos, String type) {
return callArgs('getHelper', [pos.toProxy(), type]);
}
/// Copy the content of the editor into the textarea.
///
/// Only available if the CodeMirror instance was created using the
/// `CodeMirror.fromTextArea` constructor.
void save() => call('save');
/// Remove the editor, and restore the original textarea (with the editor's
/// current content).
///
/// Only available if the CodeMirror instance was created using the
/// CodeMirror.fromTextArea` constructor.
void toTextArea() => call('toTextArea');
/// Returns the textarea that the instance was based on.
///
/// Only available if the CodeMirror instance was created using the
/// `CodeMirror.fromTextArea` constructor.
TextAreaElement getTextArea() => call('getTextArea');
/// If you create and discard a large number of `CodeMirror` instances, you
/// should call [dispose] after finishing with each one.
void dispose() {
super.dispose();
// Remove registrations from the map.
_instances.remove(jsProxy);
}
}
/// Each editor is associated with an instance of [Doc], its document. A document
/// represents the editor content, plus a selection, an undo history, and a mode.
/// A document can only be associated with a single editor at a time. You can
/// create new documents by calling the
/// `CodeMirror.Doc(text, mode, firstLineNumber)` constructor. The last two
/// arguments are optional and can be used to set a mode for the document and
/// make it start at a line number other than 0, respectively.
class Doc extends ProxyHolder {
static JsObject _create(String text, String mode, int firstLineNumber) {
if (firstLineNumber == null) {
return JsObject(context['CodeMirror']['Doc'], [text, mode]);
} else {
return JsObject(
context['CodeMirror']['Doc'], [text, mode, firstLineNumber]);
}
}
CodeMirror _editor;
Doc(String text, [String mode, int firstLineNumber])
: super(_create(text, mode, firstLineNumber));
Doc.fromProxy(JsObject proxy) : super(proxy);
CodeMirror getEditor() {
if (_editor == null) {
_editor = CodeMirror.fromJsObject(call('getEditor'));
}
return _editor;
}
/// Get the current editor content. You can pass it an optional argument to
/// specify the string to be used to separate lines (defaults to "\n").
String getValue([String separator]) => callArg('getValue', separator);
/// Set the editor content.
void setValue(String value) => callArg('setValue', value);
/// Get the number of lines in the editor.
int lineCount() => call('lineCount');
/// Get the first line of the editor. This will usually be zero but for linked
/// sub-views, or documents instantiated with a non-zero first line, it might
/// return other values.
int firstLine() => call('firstLine');
/// Get the last line of the editor. This will usually be doc.lineCount() - 1,
/// but for linked sub-views, it might return other values.
int lastLine() => call('lastLine');
/// Get the content of line n.
String getLine(int n) => callArg('getLine', n);
/// Iterate over the whole document, or if [start] and [end] line numbers are
/// given, the range from start up to (not including) end, and call f for each
/// line, passing the line handle. This is a faster way to visit a range of
/// line handlers than calling getLineHandle for each of them. Note that line
/// handles have a text property containing the line's content (as a string).
void eachLine(LineHandler callback, {int start, int end}) {
start ??= firstLine();
end ??= lastLine() + 1;
callArgs('eachLine', [
start,
end,
(JsObject line) {
callback(LineHandle(line));
}
]);
}
/// Return `true` if any text is selected.
bool somethingSelected() => call('somethingSelected');
/// Get the currently selected code. Optionally pass a line separator to put
/// between the lines in the output. When multiple selections are present, they
/// are concatenated with instances of [lineSep] in between.
String getSelection([String lineSep]) => callArg('getSelection', lineSep);
/// Set a single selection range. anchor and head should be {line, ch} objects.
/// head defaults to anchor when not given. These options are supported:
///
/// `scroll`: determines whether the selection head should be scrolled into
/// view. Defaults to true.
///
/// `origin`: detemines whether the selection history event may be merged with
/// the previous one. When an origin starts with the character +, and the last
/// recorded selection had the same origin and was similar (close in time, both
/// collapsed or both non-collapsed), the new one will replace the old one.
/// When it starts with///, it will always replace the previous event (if that
/// had the same origin). Built-in motion uses the "+move" origin.
///
/// `bias`: determine the direction into which the selection endpoints should
/// be adjusted when they fall inside an atomic range. Can be either -1
/// (backward) or 1 (forward). When not given, the bias will be based on the
/// relative position of the old selection—the editor will try to move further
/// away from that, to prevent getting stuck.
void setSelection(Position anchor, {Position head, Map options}) {
callArgs('setSelection',
[anchor.toProxy(), head == null ? null : head.toProxy(), options]);
}
/// Replace the selection(s) with the given string. By default, the new
/// selection ends up after the inserted text. The optional select argument can
/// be used to change this. Passing `around`: will cause the new text to be
/// selected; `start`: will collapse the selection to the start of the inserted
/// text.
void replaceSelection(String replacement, [String select]) {
callArgs('replaceSelection',
select != null ? [replacement, select] : [replacement]);
}
/// Returns an array containing a string for each selection, representing the
/// content of the selections.
Iterable<String> getSelections([String lineSep]) =>
callArg('getSelections', lineSep).cast<String>();
/// Sets a new set of selections. There must be at least one selection in the
/// given array. When [primary] is a number, it determines which selection is
/// the primary one. When it is not given, the primary index is taken from
/// the previous selection, or set to the last range if the previous
/// selection had less ranges than the new one. Supports the same
/// options as [setSelection].
void setSelections(Iterable<Span> ranges, {int primary, Map options}) {
callArgs('setSelections', [
JsArray.from(ranges.map((Span range) => range.toProxy())),
primary,
options
]);
}
/// The length of the given array should be the same as the number of active
/// selections. Replaces the content of the selections with the strings in
/// the array. The select argument works the same as in [replaceSelection].
void replaceSelections(Iterable<String> replacement, {String select}) {
callArgs('replaceSelections',
select != null ? [jsify(replacement), select] : [jsify(replacement)]);
}
/// Adds a new selection to the existing set of selections, and makes it the
/// primary selection.
void addSelection({Position anchor, Position head}) {
head ??= anchor;
callArgs('addSelection', [anchor.toProxy(), head.toProxy()]);
}
/// Similar to [setSelection], but will, if shift is held or the extending flag
/// is set, move the head of the selection while leaving the anchor at its
/// current place. [to] is optional, and can be passed to ensure a region (for
/// example a word or paragraph) will end up selected (in addition to whatever
/// lies between that region and the current anchor). When multiple selections
/// are present, all but the primary selection will be dropped by this method.
/// Supports the same options as [setSelection].
void extendSelection(Position from, [Position to, Map options]) {
callArgs('extendSelection', [from.toProxy(), to?.toProxy(), options]);
}
/// An equivalent of [extendSelection] that acts on all selections at once.
void extendSelections(List<Position> heads, [Map options]) {
callArgs('extendSelections',
[JsArray.from(heads.map((Position head) => head.toProxy())), options]);
}
/// Applies the given function to all existing selections, and calls
/// [extendSelections] on the result.
void extendSelectionsBy(SelectionExtender f, [Map options]) {
callArgs('extendSelectionsBy', [
(JsObject obj, int i) => f(Span.fromProxy(obj), i).toProxy(),
options
]);
}
/// Sets or clears the 'extending' flag, which acts similar to the shift key,
/// in that it will cause cursor movement and calls to extendSelection to
/// leave the selection anchor in place.
void setExtending(bool value) {
callArg('setExtending', value);
}
/// Get the value of the 'extending' flag.
bool getExtending() => call('getExtending');
/// Retrieves a list of all current selections.
///
/// These will always be sorted,
/// and never overlap (overlapping selections are merged). Each object in the
/// array contains `anchor` and `head` properties referring
/// to `{line, ch}` objects.
Iterable<Span> listSelections() {
return call('listSelections').map((JsObject selection) {
return Span.fromProxy(selection);
});
}
/// Replace the part of the document between [from] and [to] with the given
/// string. [to] can be left off to simply insert the string at position
/// [from].
///
/// When origin is given, it will be passed on to "change" events, and its
/// first letter will be used to determine whether this change can be merged
/// with previous history events, in the way described for selection origins.
void replaceRange(String replacement, Position from,
[Position to, String origin]) {
callArgs(
'replaceRange',
origin != null
? [replacement, from.toProxy(), to.toProxy(), origin]
: [replacement, from.toProxy(), to == null ? null : to.toProxy()]);
}
/// Set the editor content as 'clean', a flag that it will retain until it is
/// edited, and which will be set again when such an edit is undone again.
/// Useful to track whether the content needs to be saved. This function is
/// deprecated in favor of changeGeneration, which allows multiple subsystems
/// to track different notions of cleanness without interfering.
void markClean() => call('markClean');
/// Returns a number that can later be passed to [isClean] to test whether any
/// edits were made (and not undone) in the meantime. If closeEvent is true,
/// the current history event will be 'closed', meaning it can't be combined
/// with further changes (rapid typing or deleting events are typically
/// combined).
int changeGeneration([bool closeEvent]) {
return closeEvent == null
? call('changeGeneration')
: callArg('changeGeneration', closeEvent);
}
/// Returns whether the document is currently clean, not modified since
/// initialization or the last call to [markClean] if no argument is passed, or
/// since the matching call to [changeGeneration] if a generation value is
/// given.
bool isClean([int generation]) {
return generation == null
? call('isClean')
: callArg('isClean', generation);
}
// History-related methods.
/// Undo one edit (if any undo events are stored).
void undo() => call('undo');
/// Redo one undone edit.
void redo() => call('redo');
/// Undo one edit or selection change.
void undoSelection() => call('undoSelection');
/// Redo one undone edit or selection change.
void redoSelection() => call('redoSelection');
/// Returns an object with `{'undo': int, 'redo': int}` properties, both of
/// which hold integers, indicating the amount of stored undo and redo
/// operations.
Map<String, int> historySize() {
JsObject result = call('historySize');
return {
'undo': result['undo'],
'redo': result['redo'],
};
}
/// Clears the editor's undo history.
void clearHistory() => call('clearHistory');
/// Get a (JSON-serializeable) representation of the undo history.
JsObject getHistory() => call('getHistory');
/// Replace the editor's undo history with the one provided, which must be a
/// value as returned by [getHistory]. Note that this will have entirely
/// undefined results if the editor content isn't also the same as it was when
/// [getHistory] was called.
void setHistory(JsObject history) => callArg('setHistory', history);
/// Retrieve one end of the primary selection. start is a an optional string
/// indicating which end of the selection to return. It may be "from", "to",
/// "head" (the side of the selection that moves when you press shift+arrow),
/// or "anchor" (the fixed side of the selection). Omitting the argument is the
/// same as passing "head". A {line, ch} object will be returned.
Position getCursor([String start]) => Position.fromProxy(
start == null ? call('getCursor') : callArg('getCursor', start));
/// Set the cursor position. You can either pass a single {line, ch} object, or
/// the line and the character as two separate parameters. Will replace all
/// selections with a single, empty selection at the given position. The
/// supported options are the same as for setSelection.
void setCursor(Position pos, {Map options}) {
callArgs('setCursor', [pos.toProxy(), options]);
}
/// Get the text between the given points in the editor. An optional third
/// argument can be given to indicate the line separator string to use
/// (defaults to "\n").
String getRange(Position from, Position to, [String separator]) {
return callArgs('getRange', [from.toProxy(), to.toProxy(), separator]);
}
/// Calculates and returns a `Position` object for a zero-based index who's
/// value is relative to the start of the editor's text. If the index is out of
/// range of the text then the returned object is clipped to start or end of
/// the text respectively.
Position posFromIndex(int index) =>
Position.fromProxy(callArg('posFromIndex', index));
/// The reverse of [posFromIndex].
int indexFromPos(Position pos) => callArg('indexFromPos', pos.toProxy());
/// Can be used to mark a range of text with a specific CSS class name.
///
/// [className]: assigns a CSS class to the marked stretch of text.
/// [inclusiveLeft]: determines whether text inserted on the left of the marker
/// will end up inside or outside of it.
/// [inclusiveRight]: like inclusiveLeft, but for the right side.
/// [atomic]: atomic ranges act as a single unit when cursor movement is
/// concerned — i.e. it is impossible to place the cursor inside of them. In
/// atomic ranges, inclusiveLeft and inclusiveRight have a different meaning —
/// they will prevent the cursor from being placed respectively directly before
/// and directly after the range.
/// [collapsed]: collapsed ranges do not show up in the display. Setting a
/// range to be collapsed will automatically make it atomic.
/// [clearOnEnter]: when enabled, will cause the mark to clear itself whenever
/// the cursor enters its range. This is mostly useful for text - replacement
/// widgets that need to 'snap open' when the user tries to edit them. The
/// "clear" event fired on the range handle can be used to be notified when
/// this happens.
/// [clearWhenEmpty]: determines whether the mark is automatically cleared when
/// it becomes empty. Default is true.
/// [replacedWith]: use a given node to display this range. Implies both
/// collapsed and atomic. The given DOM node must be an inline element (as
/// opposed to a block element).
/// [handleMouseEvents]: when replacedWith is given, this determines whether
/// the editor will capture mouse and drag events occurring in this widget.
/// Default is false — the events will be left alone for the default browser
/// handler, or specific handlers on the widget, to capture.
/// [readOnly]: a read-only span can, as long as it is not cleared, not be
/// modified except by calling setValue to reset the whole document. Note:
/// adding a read-only span currently clears the undo history of the editor,
/// because existing undo events being partially nullified by read-only spans
/// would corrupt the history (in the current implementation).
/// [addToHistory]: when set to true (default is false), adding this marker
/// will create an event in the undo history that can be individually undone
/// (clearing the marker).
/// [startStyle]: can be used to specify an extra CSS class to be applied to
/// the leftmost span that is part of the marker.
/// [endStyle]: equivalent to startStyle, but for the rightmost span.
/// [css] a string of CSS to be applied to the covered text. For example
/// "color: #fe3".
/// [title]: when given, will give the nodes created for this span a HTML title
/// attribute with the given value.
/// [shared]: when the target document is linked to other documents, you can
/// set shared to true to make the marker appear in all documents. By default,
/// a marker appears only in its target document.
TextMarker markText(Position from, Position to,
{String className,
bool inclusiveLeft,
bool inclusiveRight,
bool atomic,
bool collapsed,
bool clearOnEnter,
bool clearWhenEmpty,
Element replacedWith,
bool handleMouseEvents,
bool readOnly,
bool addToHistory,
String startStyle,
String endStyle,
String css,
String title,
bool shared}) {
Map options = {};
if (className != null) options['className'] = className;
if (inclusiveLeft != null) options['inclusiveLeft'] = inclusiveLeft;
if (inclusiveRight != null) options['inclusiveRight'] = inclusiveRight;
if (atomic != null) options['atomic'] = atomic;
if (collapsed != null) options['collapsed'] = collapsed;
if (clearOnEnter != null) options['clearOnEnter'] = clearOnEnter;
if (clearWhenEmpty != null) options['clearWhenEmpty'] = clearWhenEmpty;
if (replacedWith != null) options['replacedWith'] = replacedWith;
if (handleMouseEvents != null) {
options['handleMouseEvents'] = handleMouseEvents;
}
if (readOnly != null) options['readOnly'] = readOnly;
if (addToHistory != null) options['addToHistory'] = addToHistory;
if (startStyle != null) options['startStyle'] = startStyle;
if (endStyle != null) options['endStyle'] = endStyle;
if (css != null) options['css'] = css;
if (title != null) options['title'] = title;
if (shared != null) options['shared'] = shared;
return TextMarker(
callArgs('markText', [from.toProxy(), to.toProxy(), jsify(options)]));
}
/// Inserts a bookmark, a handle that follows the text around it as it is being
/// edited, at the given position. A bookmark has two methods find() and
/// clear(). The first returns the current position of the bookmark, if it is
/// still in the document, and the second explicitly removes the bookmark.
///
/// [widget] can be used to display a DOM node at the current location of the
/// bookmark (analogous to the replacedWith option to markText). [insertLeft]:
/// by default, text typed when the cursor is on top of the bookmark will end
/// up to the right of the bookmark. Set this option to true to make it go to
/// the left instead. [shared]: when the target document is linked to other
/// documents, you can set shared to true to make the marker appear in all
/// documents. By default, a marker appears only in its target document.
TextMarker setBookmark(Position pos,
{Element widget, bool insertLeft, bool shared}) {
Map options = {};
if (widget != null) options['widget'] = widget;
if (insertLeft != null) options['insertLeft'] = insertLeft;
if (shared != null) options['shared'] = shared;
return TextMarker(callArgs('setBookmark', [pos.toProxy(), jsify(options)]));
}
/// Returns an array of all the bookmarks and marked ranges found between the
/// given positions.
List<TextMarker> findMarks(Position from, Position to) {
var result = callArgs('findMarks', [from.toProxy(), to.toProxy()]);
if (result is! List) return [];
return List.from(result.map((mark) => TextMarker(mark)));
}
/// Returns an array of all the bookmarks and marked ranges present at the
/// given position.
List<TextMarker> findMarksAt(Position pos) {
var result = callArg('findMarksAt', pos.toProxy());
if (result is! List) return [];
return List.from(result.map((mark) => TextMarker(mark)));
}
/// Returns an array containing all marked ranges in the document.
List<TextMarker> getAllMarks() {
var result = call('getAllMarks');
if (result is! List) return [];
return List.from(result.map((mark) => TextMarker(mark)));
}
/// Gets the (outer) mode object for the editor. Note that this is distinct
/// from getOption("mode"), which gives you the mode specification, rather than
/// the resolved, instantiated mode object.
///
/// The returned mode is a `JsObject`.
dynamic getMode() => call('getMode');
/// Return the name of the current mode.
String getModeName() => getMode()['name'];
/// Gets the inner mode at a given position. This will return the same as
/// getMode for simple modes, but will return an inner mode for nesting modes
/// (such as htmlmixed).
///
/// The returned mode is a `JsObject`.
dynamic getModeAt(Position pos) =>
getEditor().callArg('getModeAt', pos.toProxy());
/// Return the name of the mode at the given position.
String getModeNameAt(Position pos) => getModeAt(pos)['name'];
/// Fetches the line handle for the given line number.
LineHandle getLineHandle(int line) {
return LineHandle(callArg('getLineHandle', line));
}
/// Given a line handle, returns the current position of that line (or `null`
/// when it is no longer in the document).
int getLineNumber(LineHandle handle) {
return callArg('getLineNumber', handle.jsProxy);
}
/// Fired whenever a change occurs to the document. `changeObj` has a similar
/// type as the object passed to the editor's "change" event.
Stream get onChange => onEvent('change', argCount: 2);
}
/// Both `line` and `ch` are 0-based.
///
/// `{line, ch}`
class Position implements Comparable<Position> {
final int line;
final int ch;
Position(this.line, this.ch);
Position.fromProxy(var obj)
: line = obj['line'],
ch = obj['ch'];
JsObject toProxy() => jsify({'line': line, 'ch': ch});
operator ==(other) =>
other is Position && line == other.line && ch == other.ch;
int get hashCode => (line << 8 | ch).hashCode;
int compareTo(Position other) {
if (line == other.line) return ch - other.ch;
return line - other.line;
}
operator <(Position other) => compareTo(other) < 0;
operator <=(Position other) => compareTo(other) <= 0;
operator >=(Position other) => compareTo(other) >= 0;
operator >(Position other) => compareTo(other) > 0;
String toString() => '[${line}:${ch}]';
}
class ModeInfo extends ProxyHolder {
factory ModeInfo(JsObject proxy) => proxy == null ? null : ModeInfo._(proxy);
ModeInfo._(JsObject proxy) : super(proxy);
/// The mode's human readable, display name.
String get name => jsProxy['name'];
String get mime => jsProxy['mime'];
List<String> get mimes =>
jsProxy.hasProperty('mimes') ? jsProxy['mimes'] : <String>[mime];
/// The mode's id.
String get mode => jsProxy['mode'];
/// The mode's file extension.
List<String> get ext => List.from(jsProxy['ext']);
/// The mode's other file extensions.
List<String> get alias =>
jsProxy.hasProperty('alias') ? jsProxy['alias'] : <String>[];
}
/// A source span from a start position ([head]) to an end position ([anchor]);
class Span {
final Position head;
final Position anchor;
Span(this.head, this.anchor);
Span.fromProxy(var obj)
: head = Position.fromProxy(obj['head']),
anchor = Position.fromProxy(obj['anchor']);
JsObject toProxy() =>
jsify({'head': head.toProxy(), 'anchor': anchor.toProxy()});
operator ==(other) =>
other is Span && head == other.head && anchor == other.anchor;
int get hashCode => head.hashCode ^ anchor.hashCode;
String toString() => '${head}=>${anchor}]';
}
/// An object that represents a marker.
class TextMarker extends ProxyHolder {
TextMarker(JsObject jsProxy) : super(jsProxy);
/// Removes the mark.
void clear() => call('clear');
/// Returns a {from, to} object (both holding document positions), indicating
/// the current position of the marked range, or `null` if the marker is no
/// longer in the document. For a bookmark, this list will be length 1.
List<Position> find() {
final result = call('find');
if (result is! JsObject) return null;
try {
if (result.hasProperty('from')) {
return [
Position.fromProxy(result['from']),
Position.fromProxy(result['to'])
];
} else {
return [Position.fromProxy(result)];
}
} catch (e) {
return null;
}
}
/// Return the first (or only) position in this marker / bookmark.
Position findStart() {
List<Position> positions = find();
return (positions == null || positions.isEmpty) ? null : positions.first;
}
/// Call if you've done something that might change the size of the marker (for
/// example changing the content of a replacedWith node), and want to cheaply
/// update the display.
void changed() => call('changed');
}
/// See [CodeMirror.addLineWidget].
class LineWidget extends ProxyHolder {
LineWidget(JsObject jsProxy) : super(jsProxy);
// TODO: add `line` property
/// Removes the widget.
void clear() => call('clear');
/// Call this if you made some change to the widget's DOM node that might
/// affect its height. It'll force CodeMirror to update the height of the line
/// that contains the widget.
void changed() => call('changed');
}
class LineHandle extends ProxyHolder {
LineHandle(JsObject jsProxy) : super(jsProxy);
num get height => jsProxy['height'];
String get text => jsProxy['text'];
}
class ScrollInfo extends ProxyHolder {
ScrollInfo(JsObject jsProxy) : super(jsProxy);
num get left => jsProxy['left'];
num get top => jsProxy['top'];
num get width => jsProxy['width'];
num get height => jsProxy['height'];
num get clientWidth => jsProxy['clientWidth'];
num get clientHeight => jsProxy['clientHeight'];
}
class Token {
/// The character (on the given line) at which the token starts.
final int start;
/// The character at which the token ends.
final int end;
/// The token's string.
final String string;
/// The token type the mode assigned to the token, such as "keyword" or
/// "comment" (may also be null).
final String type;
/// The mode's state at the end of this token.
final JsObject state;
Token.fromProxy(var obj)
: start = obj['start'],
end = obj['end'],
string = obj['string'],
type = obj['type'],
state = obj['state'];
String toString() => string;
}
/// A parent class for objects that can hold references to JavaScript objects.
/// It has convenience methods for invoking methods on the JavaScript proxy,
/// a method to add event listeners to the proxy, and a [dispose] method.
/// `dispose` only needs to be called if event listeners were added to an object.
abstract class ProxyHolder {
final JsObject jsProxy;
final Map<String, JsEventListener> _events = {};
ProxyHolder(this.jsProxy);
dynamic call(String methodName) => jsProxy.callMethod(methodName);
dynamic callArg(String methodName, var arg) =>
jsProxy.callMethod(methodName, [arg]);
dynamic callArgs(String methodName, List args) =>
jsProxy.callMethod(methodName, args);
Stream<T> onEvent<T>(String eventName, {int argCount = 1}) {
if (!_events.containsKey(eventName)) {
if (argCount == 4) {
_events[eventName] = JsEventListener<T>(jsProxy, eventName,
cvtEvent: (a, b, c) => a, argCount: argCount);
} else if (argCount == 3) {
_events[eventName] = JsEventListener<T>(jsProxy, eventName,
cvtEvent: (a, b) => a, argCount: argCount);
} else if (argCount == 2) {
_events[eventName] =
JsEventListener<T>(jsProxy, eventName, argCount: argCount);
} else {
_events[eventName] = JsEventListener<T>(jsProxy, eventName);
}
}
return _events[eventName].stream;
}
int get hashCode => jsProxy.hashCode;
operator ==(other) => other is ProxyHolder && jsProxy == other.jsProxy;
/// This method should be called if any events listeners were added to the
/// object.
void dispose() {
if (_events.isNotEmpty) {
for (JsEventListener event in _events.values) {
event.dispose();
}
}
}
}