summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/sourceeditor
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
commita90a5cba08fdf6c0ceb95101c275108a152a3aed (patch)
tree532507288f3defd7f4dcf1af49698bcb76034855 /devtools/client/shared/sourceeditor
parentAdding debian version 126.0.1-1. (diff)
downloadfirefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.tar.xz
firefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/sourceeditor')
-rw-r--r--devtools/client/shared/sourceeditor/css-autocompleter.js578
-rw-r--r--devtools/client/shared/sourceeditor/editor.js324
2 files changed, 595 insertions, 307 deletions
diff --git a/devtools/client/shared/sourceeditor/css-autocompleter.js b/devtools/client/shared/sourceeditor/css-autocompleter.js
index 7db3cbbc0f..4266bd02b8 100644
--- a/devtools/client/shared/sourceeditor/css-autocompleter.js
+++ b/devtools/client/shared/sourceeditor/css-autocompleter.js
@@ -28,15 +28,14 @@ const {
* The file 'css-parsing-utils' helps to convert the CSS into meaningful tokens,
* each having a certain type associated with it. These tokens help us to figure
* out the currently edited word and to write a CSS state machine to figure out
- * what the user is currently editing. By that, I mean, whether he is editing a
- * selector or a property or a value, or even fine grained information like an
- * id in the selector.
+ * what the user is currently editing (e.g. a selector or a property or a value,
+ * or even fine grained information like an id in the selector).
*
* The `resolveState` method iterated over the tokens spitted out by the
* tokenizer, using switch cases, follows a state machine logic and finally
* figures out these informations:
* - The state of the CSS at the cursor (one out of CSS_STATES)
- * - The current token that is being edited `cmpleting`
+ * - The current token that is being edited `completing`
* - If the state is "selector", the selector state (one of SELECTOR_STATES)
* - If the state is "selector", the current selector till the cursor
* - If the state is "value", the corresponding property name
@@ -214,30 +213,26 @@ CSSCompleter.prototype = {
// From CSS_STATES.property, we can either go to CSS_STATES.value
// state when we hit the first ':' or CSS_STATES.selector if "}" is
// reached.
- if (token.tokenType === "symbol") {
- switch (token.text) {
- case ":":
- scopeStack.push(":");
- if (tokens[cursor - 2].tokenType != "whitespace") {
- propertyName = tokens[cursor - 2].text;
- } else {
- propertyName = tokens[cursor - 3].text;
- }
- _state = CSS_STATES.value;
- break;
+ if (token.tokenType === "Colon") {
+ scopeStack.push(":");
+ if (tokens[cursor - 2].tokenType != "WhiteSpace") {
+ propertyName = tokens[cursor - 2].text;
+ } else {
+ propertyName = tokens[cursor - 3].text;
+ }
+ _state = CSS_STATES.value;
+ }
- case "}":
- if (/[{f]/.test(peek(scopeStack))) {
- const popped = scopeStack.pop();
- if (popped == "f") {
- _state = CSS_STATES.frame;
- } else {
- selector = "";
- selectors = [];
- _state = CSS_STATES.null;
- }
- }
- break;
+ if (token.tokenType === "CloseCurlyBracket") {
+ if (/[{f]/.test(peek(scopeStack))) {
+ const popped = scopeStack.pop();
+ if (popped == "f") {
+ _state = CSS_STATES.frame;
+ } else {
+ selector = "";
+ selectors = [];
+ _state = CSS_STATES.null;
+ }
}
}
break;
@@ -245,31 +240,27 @@ CSSCompleter.prototype = {
case CSS_STATES.value:
// From CSS_STATES.value, we can go to one of CSS_STATES.property,
// CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null
- if (token.tokenType === "symbol") {
- switch (token.text) {
- case ";":
- if (/[:]/.test(peek(scopeStack))) {
- scopeStack.pop();
- _state = CSS_STATES.property;
- }
- break;
+ if (token.tokenType === "Semicolon") {
+ if (/[:]/.test(peek(scopeStack))) {
+ scopeStack.pop();
+ _state = CSS_STATES.property;
+ }
+ }
- case "}":
- if (peek(scopeStack) == ":") {
- scopeStack.pop();
- }
+ if (token.tokenType === "CloseCurlyBracket") {
+ if (peek(scopeStack) == ":") {
+ scopeStack.pop();
+ }
- if (/[{f]/.test(peek(scopeStack))) {
- const popped = scopeStack.pop();
- if (popped == "f") {
- _state = CSS_STATES.frame;
- } else {
- selector = "";
- selectors = [];
- _state = CSS_STATES.null;
- }
- }
- break;
+ if (/[{f]/.test(peek(scopeStack))) {
+ const popped = scopeStack.pop();
+ if (popped == "f") {
+ _state = CSS_STATES.frame;
+ } else {
+ selector = "";
+ selectors = [];
+ _state = CSS_STATES.null;
+ }
}
}
break;
@@ -277,7 +268,7 @@ CSSCompleter.prototype = {
case CSS_STATES.selector:
// From CSS_STATES.selector, we can only go to CSS_STATES.property
// when we hit "{"
- if (token.tokenType === "symbol" && token.text == "{") {
+ if (token.tokenType === "CurlyBracketBlock") {
scopeStack.push("{");
_state = CSS_STATES.property;
selectors.push(selector);
@@ -290,74 +281,87 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.class:
case SELECTOR_STATES.tag:
switch (token.tokenType) {
- case "hash":
- case "id":
+ case "Hash":
+ case "IDHash":
selectorState = SELECTOR_STATES.id;
- selector += "#" + token.text;
+ selector += token.text;
break;
- case "symbol":
+ case "Delim":
if (token.text == ".") {
selectorState = SELECTOR_STATES.class;
selector += ".";
if (
cursor <= tokIndex &&
- tokens[cursor].tokenType == "ident"
+ tokens[cursor].tokenType == "Ident"
) {
token = tokens[cursor++];
selector += token.text;
}
} else if (token.text == "#") {
+ // Lonely # char, that doesn't produce a Hash nor IDHash
selectorState = SELECTOR_STATES.id;
selector += "#";
- } else if (/[>~+]/.test(token.text)) {
+ } else if (
+ token.text == "+" ||
+ token.text == "~" ||
+ token.text == ">"
+ ) {
selectorState = SELECTOR_STATES.null;
selector += token.text;
- } else if (token.text == ",") {
- selectorState = SELECTOR_STATES.null;
- selectors.push(selector);
- selector = "";
- } else if (token.text == ":") {
- selectorState = SELECTOR_STATES.pseudo;
- selector += ":";
- if (cursor > tokIndex) {
- break;
- }
+ }
+ break;
- token = tokens[cursor++];
- switch (token.tokenType) {
- case "function":
- if (token.text == "not") {
- selectorBeforeNot = selector;
- selector = "";
- scopeStack.push("(");
- } else {
- selector += token.text + "(";
- }
- selectorState = SELECTOR_STATES.null;
- break;
-
- case "ident":
+ case "Comma":
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ break;
+
+ case "Colon":
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "Function":
+ if (token.value == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
selector += token.text;
- break;
- }
- } else if (token.text == "[") {
- selectorState = SELECTOR_STATES.attribute;
- scopeStack.push("[");
- selector += "[";
- } else if (token.text == ")") {
- if (peek(scopeStack) == "(") {
- scopeStack.pop();
- selector = selectorBeforeNot + "not(" + selector + ")";
- selectorBeforeNot = null;
- } else {
- selector += ")";
- }
- selectorState = SELECTOR_STATES.null;
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "Ident":
+ selector += token.text;
+ break;
+ }
+ break;
+
+ case "SquareBracketBlock":
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ break;
+
+ case "CloseParenthesis":
+ if (peek(scopeStack) == "(") {
+ scopeStack.pop();
+ selector = selectorBeforeNot + "not(" + selector + ")";
+ selectorBeforeNot = null;
+ } else {
+ selector += ")";
}
+ selectorState = SELECTOR_STATES.null;
break;
- case "whitespace":
+ case "WhiteSpace":
selectorState = SELECTOR_STATES.null;
selector && (selector += " ");
break;
@@ -369,81 +373,94 @@ CSSCompleter.prototype = {
// SELECTOR_STATES.id, SELECTOR_STATES.class or
// SELECTOR_STATES.tag
switch (token.tokenType) {
- case "hash":
- case "id":
+ case "Hash":
+ case "IDHash":
selectorState = SELECTOR_STATES.id;
- selector += "#" + token.text;
+ selector += token.text;
break;
- case "ident":
+ case "Ident":
selectorState = SELECTOR_STATES.tag;
selector += token.text;
break;
- case "symbol":
+ case "Delim":
if (token.text == ".") {
selectorState = SELECTOR_STATES.class;
selector += ".";
if (
cursor <= tokIndex &&
- tokens[cursor].tokenType == "ident"
+ tokens[cursor].tokenType == "Ident"
) {
token = tokens[cursor++];
selector += token.text;
}
} else if (token.text == "#") {
+ // Lonely # char, that doesn't produce a Hash nor IDHash
selectorState = SELECTOR_STATES.id;
selector += "#";
} else if (token.text == "*") {
selectorState = SELECTOR_STATES.tag;
selector += "*";
- } else if (/[>~+]/.test(token.text)) {
+ } else if (
+ token.text == "+" ||
+ token.text == "~" ||
+ token.text == ">"
+ ) {
selector += token.text;
- } else if (token.text == ",") {
- selectorState = SELECTOR_STATES.null;
- selectors.push(selector);
- selector = "";
- } else if (token.text == ":") {
- selectorState = SELECTOR_STATES.pseudo;
- selector += ":";
- if (cursor > tokIndex) {
- break;
- }
+ }
+ break;
- token = tokens[cursor++];
- switch (token.tokenType) {
- case "function":
- if (token.text == "not") {
- selectorBeforeNot = selector;
- selector = "";
- scopeStack.push("(");
- } else {
- selector += token.text + "(";
- }
- selectorState = SELECTOR_STATES.null;
- break;
-
- case "ident":
+ case "Comma":
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ break;
+
+ case "Colon":
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "Function":
+ if (token.value == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
selector += token.text;
- break;
- }
- } else if (token.text == "[") {
- selectorState = SELECTOR_STATES.attribute;
- scopeStack.push("[");
- selector += "[";
- } else if (token.text == ")") {
- if (peek(scopeStack) == "(") {
- scopeStack.pop();
- selector = selectorBeforeNot + "not(" + selector + ")";
- selectorBeforeNot = null;
- } else {
- selector += ")";
- }
- selectorState = SELECTOR_STATES.null;
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "Ident":
+ selector += token.text;
+ break;
}
break;
- case "whitespace":
+ case "SquareBracketBlock":
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ break;
+
+ case "CloseParenthesis":
+ if (peek(scopeStack) == "(") {
+ scopeStack.pop();
+ selector = selectorBeforeNot + "not(" + selector + ")";
+ selectorBeforeNot = null;
+ } else {
+ selector += ")";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "WhiteSpace":
selector && (selector += " ");
break;
}
@@ -451,46 +468,55 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.pseudo:
switch (token.tokenType) {
- case "symbol":
- if (/[>~+]/.test(token.text)) {
+ case "Delim":
+ if (
+ token.text == "+" ||
+ token.text == "~" ||
+ token.text == ">"
+ ) {
selectorState = SELECTOR_STATES.null;
selector += token.text;
- } else if (token.text == ",") {
- selectorState = SELECTOR_STATES.null;
- selectors.push(selector);
- selector = "";
- } else if (token.text == ":") {
- selectorState = SELECTOR_STATES.pseudo;
- selector += ":";
- if (cursor > tokIndex) {
- break;
- }
+ }
+ break;
+
+ case "Comma":
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ break;
+
+ case "Colon":
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
- token = tokens[cursor++];
- switch (token.tokenType) {
- case "function":
- if (token.text == "not") {
- selectorBeforeNot = selector;
- selector = "";
- scopeStack.push("(");
- } else {
- selector += token.text + "(";
- }
- selectorState = SELECTOR_STATES.null;
- break;
-
- case "ident":
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "Function":
+ if (token.value == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
selector += token.text;
- break;
- }
- } else if (token.text == "[") {
- selectorState = SELECTOR_STATES.attribute;
- scopeStack.push("[");
- selector += "[";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "Ident":
+ selector += token.text;
+ break;
}
break;
+ case "SquareBracketBlock":
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ break;
- case "whitespace":
+ case "WhiteSpace":
selectorState = SELECTOR_STATES.null;
selector && (selector += " ");
break;
@@ -499,29 +525,40 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.attribute:
switch (token.tokenType) {
- case "symbol":
- if (/[~|^$*]/.test(token.text)) {
- selector += token.text;
- token = tokens[cursor++];
- } else if (token.text == "=") {
+ case "IncludeMatch":
+ case "DashMatch":
+ case "PrefixMatch":
+ case "IncludeSuffixMatchMatch":
+ case "SubstringMatch":
+ selector += token.text;
+ token = tokens[cursor++];
+ break;
+
+ case "Delim":
+ if (token.text == "=") {
selectorState = SELECTOR_STATES.value;
selector += token.text;
- } else if (token.text == "]") {
- if (peek(scopeStack) == "[") {
- scopeStack.pop();
- }
+ }
+ break;
- selectorState = SELECTOR_STATES.null;
- selector += "]";
+ case "CloseSquareBracket":
+ if (peek(scopeStack) == "[") {
+ scopeStack.pop();
}
+
+ selectorState = SELECTOR_STATES.null;
+ selector += "]";
break;
- case "ident":
- case "string":
+ case "Ident":
selector += token.text;
break;
- case "whitespace":
+ case "QuotedString":
+ selector += token.value;
+ break;
+
+ case "WhiteSpace":
selector && (selector += " ");
break;
}
@@ -529,23 +566,24 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.value:
switch (token.tokenType) {
- case "string":
- case "ident":
+ case "Ident":
selector += token.text;
break;
- case "symbol":
- if (token.text == "]") {
- if (peek(scopeStack) == "[") {
- scopeStack.pop();
- }
+ case "QuotedString":
+ selector += token.value;
+ break;
- selectorState = SELECTOR_STATES.null;
- selector += "]";
+ case "CloseSquareBracket":
+ if (peek(scopeStack) == "[") {
+ scopeStack.pop();
}
+
+ selectorState = SELECTOR_STATES.null;
+ selector += "]";
break;
- case "whitespace":
+ case "WhiteSpace":
selector && (selector += " ");
break;
}
@@ -557,29 +595,30 @@ CSSCompleter.prototype = {
// From CSS_STATES.null state, we can go to either CSS_STATES.media or
// CSS_STATES.selector.
switch (token.tokenType) {
- case "hash":
- case "id":
+ case "Hash":
+ case "IDHash":
selectorState = SELECTOR_STATES.id;
- selector = "#" + token.text;
+ selector = token.text;
_state = CSS_STATES.selector;
break;
- case "ident":
+ case "Ident":
selectorState = SELECTOR_STATES.tag;
selector = token.text;
_state = CSS_STATES.selector;
break;
- case "symbol":
+ case "Delim":
if (token.text == ".") {
selectorState = SELECTOR_STATES.class;
selector = ".";
_state = CSS_STATES.selector;
- if (cursor <= tokIndex && tokens[cursor].tokenType == "ident") {
+ if (cursor <= tokIndex && tokens[cursor].tokenType == "Ident") {
token = tokens[cursor++];
selector += token.text;
}
} else if (token.text == "#") {
+ // Lonely # char, that doesn't produce a Hash nor IDHash
selectorState = SELECTOR_STATES.id;
selector = "#";
_state = CSS_STATES.selector;
@@ -587,45 +626,52 @@ CSSCompleter.prototype = {
selectorState = SELECTOR_STATES.tag;
selector = "*";
_state = CSS_STATES.selector;
- } else if (token.text == ":") {
- _state = CSS_STATES.selector;
- selectorState = SELECTOR_STATES.pseudo;
- selector += ":";
- if (cursor > tokIndex) {
- break;
- }
+ }
+ break;
- token = tokens[cursor++];
- switch (token.tokenType) {
- case "function":
- if (token.text == "not") {
- selectorBeforeNot = selector;
- selector = "";
- scopeStack.push("(");
- } else {
- selector += token.text + "(";
- }
- selectorState = SELECTOR_STATES.null;
- break;
+ case "Colon":
+ _state = CSS_STATES.selector;
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
- case "ident":
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "Function":
+ if (token.value == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
selector += token.text;
- break;
- }
- } else if (token.text == "[") {
- _state = CSS_STATES.selector;
- selectorState = SELECTOR_STATES.attribute;
- scopeStack.push("[");
- selector += "[";
- } else if (token.text == "}") {
- if (peek(scopeStack) == "@m") {
- scopeStack.pop();
- }
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "Ident":
+ selector += token.text;
+ break;
+ }
+ break;
+
+ case "CloseSquareBracket":
+ _state = CSS_STATES.selector;
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ break;
+
+ case "CurlyBracketBlock":
+ if (peek(scopeStack) == "@m") {
+ scopeStack.pop();
}
break;
- case "at":
- _state = token.text.startsWith("m")
+ case "AtKeyword":
+ // XXX: We should probably handle other at-rules (@container, @property, …)
+ _state = token.value.startsWith("m")
? CSS_STATES.media
: CSS_STATES.keyframes;
break;
@@ -635,7 +681,7 @@ CSSCompleter.prototype = {
case CSS_STATES.media:
// From CSS_STATES.media, we can only go to CSS_STATES.null state when
// we hit the first '{'
- if (token.tokenType == "symbol" && token.text == "{") {
+ if (token.tokenType == "CurlyBracketBlock") {
scopeStack.push("@m");
_state = CSS_STATES.null;
}
@@ -644,7 +690,7 @@ CSSCompleter.prototype = {
case CSS_STATES.keyframes:
// From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state
// when we hit the first '{'
- if (token.tokenType == "symbol" && token.text == "{") {
+ if (token.tokenType == "CurlyBracketBlock") {
scopeStack.push("@k");
_state = CSS_STATES.frame;
}
@@ -654,17 +700,15 @@ CSSCompleter.prototype = {
// From CSS_STATES.frame, we can either go to CSS_STATES.property
// state when we hit the first '{' or to CSS_STATES.selector when we
// hit '}'
- if (token.tokenType == "symbol") {
- if (token.text == "{") {
- scopeStack.push("f");
- _state = CSS_STATES.property;
- } else if (token.text == "}") {
- if (peek(scopeStack) == "@k") {
- scopeStack.pop();
- }
-
- _state = CSS_STATES.null;
+ if (token.tokenType == "CurlyBracketBlock") {
+ scopeStack.push("f");
+ _state = CSS_STATES.property;
+ } else if (token.tokenType == "CloseCurlyBracket") {
+ if (peek(scopeStack) == "@k") {
+ scopeStack.pop();
}
+
+ _state = CSS_STATES.null;
}
break;
}
@@ -688,6 +732,8 @@ CSSCompleter.prototype = {
this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]);
}
}
+ // ^ while loop end
+
this.state = _state;
this.propertyName = _state == CSS_STATES.value ? propertyName : null;
this.selectorState = _state == CSS_STATES.selector ? selectorState : null;
@@ -701,10 +747,18 @@ CSSCompleter.prototype = {
}
this.selectors = selectors;
- if (token && token.tokenType != "whitespace") {
+ if (token && token.tokenType != "WhiteSpace") {
let text;
- if (token.tokenType == "dimension" || !token.text) {
+ if (token.tokenType == "Dimension" || !token.text) {
text = source.substring(token.startOffset, token.endOffset);
+ } else if (
+ token.tokenType === "IDHash" ||
+ token.tokenType === "Hash" ||
+ token.tokenType === "AtKeyword" ||
+ token.tokenType === "Function" ||
+ token.tokenType === "QuotedString"
+ ) {
+ text = token.value;
} else {
text = token.text;
}
@@ -1047,10 +1101,10 @@ CSSCompleter.prototype = {
}
let prevToken = undefined;
- const tokens = cssTokenizer(lineText);
+ const tokensIterator = cssTokenizer(lineText);
let found = false;
const ech = line == caret.line ? caret.ch : 0;
- for (let token of tokens) {
+ for (let token of tokensIterator) {
// If the line is completely spaces, handle it differently
if (lineText.trim() == "") {
limitedSource += lineText;
@@ -1061,8 +1115,8 @@ CSSCompleter.prototype = {
);
}
- // Whitespace cannot change state.
- if (token.tokenType == "whitespace") {
+ // WhiteSpace cannot change state.
+ if (token.tokenType == "WhiteSpace") {
prevToken = token;
continue;
}
@@ -1072,7 +1126,7 @@ CSSCompleter.prototype = {
ch: token.endOffset + ech,
});
if (check(forwState)) {
- if (prevToken && prevToken.tokenType == "whitespace") {
+ if (prevToken && prevToken.tokenType == "WhiteSpace") {
token = prevToken;
}
location = {
@@ -1123,8 +1177,8 @@ CSSCompleter.prototype = {
limitedSource = limitedSource.slice(0, -1 * length);
}
- // Whitespace cannot change state.
- if (token.tokenType == "whitespace") {
+ // WhiteSpace cannot change state.
+ if (token.tokenType == "WhiteSpace") {
continue;
}
@@ -1133,7 +1187,7 @@ CSSCompleter.prototype = {
ch: token.startOffset,
});
if (check(backState)) {
- if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") {
+ if (tokens[i + 1] && tokens[i + 1].tokenType == "WhiteSpace") {
token = tokens[i + 1];
}
location = {
diff --git a/devtools/client/shared/sourceeditor/editor.js b/devtools/client/shared/sourceeditor/editor.js
index 3487acffa4..90e9f6e373 100644
--- a/devtools/client/shared/sourceeditor/editor.js
+++ b/devtools/client/shared/sourceeditor/editor.js
@@ -168,6 +168,7 @@ class Editor extends EventEmitter {
#win;
#lineGutterMarkers = new Map();
#lineContentMarkers = new Map();
+ #lineContentEventHandlers = {};
#updateListener = null;
@@ -676,7 +677,9 @@ class Editor extends EventEmitter {
}
}),
lineNumberMarkersCompartment.of([]),
- lineContentMarkerCompartment.of(this.#lineContentMarkersExtension([])),
+ lineContentMarkerCompartment.of(
+ this.#lineContentMarkersExtension({ markers: [] })
+ ),
// keep last so other extension take precedence
codemirror.minimalSetup,
];
@@ -696,29 +699,51 @@ class Editor extends EventEmitter {
/**
* This creates the extension used to manage the rendering of markers
* for in editor line content.
- * @param {Array} markers - The current list of markers
+ * @param {Array} markers - The current list of markers
+ * @param {Object} domEventHandlers - A dictionary of handlers for the DOM events
+ * See https://codemirror.net/docs/ref/#view.PluginSpec.eventHandlers
* @returns {Array<ViewPlugin>} showLineContentDecorations - An extension which is an array containing the view
* which manages the rendering of the line content markers.
*/
- #lineContentMarkersExtension(markers) {
+ #lineContentMarkersExtension({ markers, domEventHandlers }) {
const {
- codemirrorView: { Decoration, ViewPlugin },
- codemirrorState: { RangeSetBuilder },
+ codemirrorView: { Decoration, ViewPlugin, WidgetType },
+ codemirrorState: { RangeSetBuilder, RangeSet },
} = this.#CodeMirror6;
+ class LineContentWidget extends WidgetType {
+ constructor(line, createElementNode) {
+ super();
+ this.toDOM = () => createElementNode(line);
+ }
+ }
+
// Build and return the decoration set
function buildDecorations(view) {
+ if (!markers) {
+ return RangeSet.empty;
+ }
const builder = new RangeSetBuilder();
for (const { from, to } of view.visibleRanges) {
for (let pos = from; pos <= to; ) {
const line = view.state.doc.lineAt(pos);
- for (const { lineClassName, condition } of markers) {
- if (condition(line.number)) {
- builder.add(
- line.from,
- line.from,
- Decoration.line({ class: lineClassName })
- );
+ for (const marker of markers) {
+ if (marker.condition(line.number)) {
+ if (marker.lineClassName) {
+ const classDecoration = Decoration.line({
+ class: marker.lineClassName,
+ });
+ builder.add(line.from, line.from, classDecoration);
+ }
+ if (marker.createLineElementNode) {
+ const nodeDecoration = Decoration.widget({
+ widget: new LineContentWidget(
+ line.number,
+ marker.createLineElementNode
+ ),
+ });
+ builder.add(line.to, line.to, nodeDecoration);
+ }
}
}
pos = line.to + 1;
@@ -727,9 +752,9 @@ class Editor extends EventEmitter {
return builder.finish();
}
- // The view which handles rendering and updating the
+ // The view which handles events, rendering and updating the
// markers decorations
- const showLineContentDecorations = ViewPlugin.fromClass(
+ const lineContentMarkersView = ViewPlugin.fromClass(
class {
decorations;
constructor(view) {
@@ -741,10 +766,46 @@ class Editor extends EventEmitter {
}
}
},
- { decorations: v => v.decorations }
+ {
+ decorations: v => v.decorations,
+ eventHandlers: domEventHandlers || this.#lineContentEventHandlers,
+ }
);
- return [showLineContentDecorations];
+ return [lineContentMarkersView];
+ }
+
+ /**
+ *
+ * @param {Object} domEventHandlers - A dictionary of handlers for the DOM events
+ */
+ setContentEventListeners(domEventHandlers) {
+ const cm = editors.get(this);
+
+ for (const eventName in domEventHandlers) {
+ const handler = domEventHandlers[eventName];
+ domEventHandlers[eventName] = (event, editor) => {
+ // Wait a cycle so the codemirror updates to the current cursor position,
+ // information, TODO: Currently noticed this issue with CM6, not ideal but should
+ // investigate further Bug 1890895.
+ event.target.ownerGlobal.setTimeout(() => {
+ const view = editor.viewState;
+ const head = view.state.selection.main.head;
+ const cursor = view.state.doc.lineAt(head);
+ const column = head - cursor.from;
+ handler(event, view, cursor.number, column);
+ }, 0);
+ };
+ }
+
+ // Cache the handlers related to the editor content
+ this.#lineContentEventHandlers = domEventHandlers;
+
+ cm.dispatch({
+ effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
+ this.#lineContentMarkersExtension({ domEventHandlers })
+ ),
+ });
}
/**
@@ -754,6 +815,9 @@ class Editor extends EventEmitter {
* @property {string} marker.lineClassName - The css class to add to the line
* @property {function} marker.condition - The condition that decides if the marker/class gets added or removed.
* The line is passed as an argument.
+ * @property {function} marker.createLineElementNode - This should return the DOM element which
+ * is used for the marker. The line number is passed as a parameter.
+ * This is optional.
*/
setLineContentMarker(marker) {
const cm = editors.get(this);
@@ -761,9 +825,9 @@ class Editor extends EventEmitter {
cm.dispatch({
effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
- this.#lineContentMarkersExtension(
- Array.from(this.#lineContentMarkers.values())
- )
+ this.#lineContentMarkersExtension({
+ markers: Array.from(this.#lineContentMarkers.values()),
+ })
),
});
}
@@ -778,9 +842,9 @@ class Editor extends EventEmitter {
cm.dispatch({
effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
- this.#lineContentMarkersExtension(
- Array.from(this.#lineContentMarkers.values())
- )
+ this.#lineContentMarkersExtension({
+ markers: Array.from(this.#lineContentMarkers.values()),
+ })
),
});
}
@@ -869,31 +933,31 @@ class Editor extends EventEmitter {
// (representing the lines in the current viewport) and generate a new rangeset for updating the line gutter
// based on the conditions defined in the markers(for each line) provided.
const builder = new RangeSetBuilder();
- for (const { from, to } of cm.visibleRanges) {
- for (let pos = from; pos <= to; ) {
- const line = cm.state.doc.lineAt(pos);
- for (const {
- lineClassName,
- condition,
- createLineElementNode,
- } of markers) {
- if (typeof condition !== "function") {
- throw new Error("The `condition` is not a valid function");
- }
- if (condition(line.number)) {
- builder.add(
- line.from,
- line.to,
- new LineGutterMarker(
- lineClassName,
- line.number,
- createLineElementNode
- )
- );
- }
+ const { from, to } = cm.viewport;
+ let pos = from;
+ while (pos <= to) {
+ const line = cm.state.doc.lineAt(pos);
+ for (const {
+ lineClassName,
+ condition,
+ createLineElementNode,
+ } of markers) {
+ if (typeof condition !== "function") {
+ throw new Error("The `condition` is not a valid function");
+ }
+ if (condition(line.number)) {
+ builder.add(
+ line.from,
+ line.to,
+ new LineGutterMarker(
+ lineClassName,
+ line.number,
+ createLineElementNode
+ )
+ );
}
- pos = line.to + 1;
}
+ pos = line.to + 1;
}
// To update the state with the newly generated marker range set, a dispatch is called on the view
@@ -907,6 +971,61 @@ class Editor extends EventEmitter {
}
/**
+ * Gets the position information for the current selection
+ * @returns {Object} cursor - The location information for the current selection
+ * cursor.from - An object with the starting line / column of the selection
+ * cursor.to - An object with the end line / column of the selection
+ */
+ getSelectionCursor() {
+ const cm = editors.get(this);
+ if (this.config.cm6) {
+ const selection = cm.state.selection.ranges[0];
+ const lineFrom = cm.state.doc.lineAt(selection.from);
+ const lineTo = cm.state.doc.lineAt(selection.to);
+ return {
+ from: {
+ line: lineFrom.number,
+ ch: selection.from - lineFrom.from,
+ },
+ to: {
+ line: lineTo.number,
+ ch: selection.to - lineTo.from,
+ },
+ };
+ }
+ return {
+ from: cm.getCursor("from"),
+ to: cm.getCursor("to"),
+ };
+ }
+
+ /**
+ * Gets the text content for the current selection
+ * @returns {String}
+ */
+ getSelectedText() {
+ const cm = editors.get(this);
+ if (this.config.cm6) {
+ const selection = cm.state.selection.ranges[0];
+ return cm.state.doc.sliceString(selection.from, selection.to);
+ }
+ return cm.getSelection().trim();
+ }
+
+ /**
+ * Check that text is selected
+ * @returns {Boolean}
+ */
+ isTextSelected() {
+ const cm = editors.get(this);
+ if (this.config.cm6) {
+ const selection = cm.state.selection.ranges[0];
+ return selection.from !== selection.to;
+ }
+ return cm.somethingSelected();
+ }
+
+ /**
* Returns a boolean indicating whether the editor is ready to
* use. Use appendTo(el).then(() => {}) for most cases
*/
@@ -1860,6 +1979,119 @@ class Editor extends EventEmitter {
}
/**
+ * This checks if the specified position (top/left) is within the current viewpport
+ * bounds. it helps determine is scrolling should happen.
+ * @param {Object} cm - The codemirror instance
+ * @param {Number} line - The line in the source
+ * @param {Number} column - The column in the source
+ * @returns {Boolean}
+ */
+ #isVisible(cm, line, column) {
+ let inXView, inYView;
+
+ function withinBounds(x, min, max) {
+ return x >= min && x <= max;
+ }
+
+ if (this.config.cm6) {
+ const pos = this.#posToOffset(cm.state.doc, line, column);
+ const coords = pos && cm.coordsAtPos(pos);
+ if (!coords) {
+ return false;
+ }
+ const { scrollTop, scrollLeft, clientHeight, clientWidth } = cm.scrollDOM;
+
+ inXView = withinBounds(coords.left, scrollLeft, scrollLeft + clientWidth);
+ inYView = withinBounds(coords.top, scrollTop, scrollTop + clientHeight);
+ } else {
+ const { top, left } = cm.charCoords({ line, ch: column }, "local");
+ const scrollArea = cm.getScrollInfo();
+ const charWidth = cm.defaultCharWidth();
+ const fontHeight = cm.defaultTextHeight();
+ const { scrollTop, scrollLeft } = cm.doc;
+
+ inXView = withinBounds(
+ left,
+ scrollLeft,
+ // Note: 30 might relate to the margin on one of the scroll bar elements.
+ // See comment https://github.com/firefox-devtools/debugger/pull/5182#discussion_r163439209
+ scrollLeft + (scrollArea.clientWidth - 30) - charWidth
+ );
+ inYView = withinBounds(
+ top,
+ scrollTop,
+ scrollTop + scrollArea.clientHeight - fontHeight
+ );
+ }
+ return inXView && inYView;
+ }
+
+ /**
+ * Converts line/col to CM6 offset position
+ * @param {Object} doc - the codemirror document
+ * @param {Number} line - The line in the source
+ * @param {Number} col - The column in the source
+ * @returns {Number}
+ */
+ #posToOffset(doc, line, col) {
+ if (!this.config.cm6) {
+ throw new Error("This function is only compatible with CM6");
+ }
+ try {
+ const offset = doc.line(line);
+ return offset.from + col;
+ } catch (e) {
+ // Line likey does not exist in viewport yet
+ console.warn(e.message);
+ }
+ return null;
+ }
+
+ /**
+ * Scrolls the editor to the specified line and column
+ * @param {Number} line - The line in the source
+ * @param {Number} column - The column in the source
+ */
+ scrollTo(line, column) {
+ const cm = editors.get(this);
+ if (this.config.cm6) {
+ const {
+ codemirrorView: { EditorView },
+ } = this.#CodeMirror6;
+
+ if (!this.#isVisible(cm, line, column)) {
+ const offset = this.#posToOffset(cm.state.doc, line, column);
+ if (!offset) {
+ return;
+ }
+ cm.dispatch({
+ effects: EditorView.scrollIntoView(offset, {
+ x: "nearest",
+ y: "center",
+ }),
+ });
+ }
+ } else {
+ // For all cases where these are on the first line and column,
+ // avoid the possibly slow computation of cursor location on large bundles.
+ if (!line && !column) {
+ cm.scrollTo(0, 0);
+ return;
+ }
+
+ const { top, left } = cm.charCoords({ line, ch: column }, "local");
+
+ if (!this.#isVisible(cm, line, column)) {
+ const scroller = cm.getScrollerElement();
+ const centeredX = Math.max(left - scroller.offsetWidth / 2, 0);
+ const centeredY = Math.max(top - scroller.offsetHeight / 2, 0);
+
+ cm.scrollTo(centeredX, centeredY);
+ }
+ }
+ }
+
+ /**
* Extends an instance of the Editor object with additional
* functions. Each function will be called with context as
* the first argument. Context is a {ed, cm} object where
@@ -1901,6 +2133,8 @@ class Editor extends EventEmitter {
this.#ownerDoc = null;
this.#updateListener = null;
this.#lineGutterMarkers.clear();
+ this.#lineContentMarkers.clear();
+ this.#lineContentEventHandlers = {};
if (this.#prefObserver) {
this.#prefObserver.off(KEYMAP_PREF, this.setKeyMap);