Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- **`src/cacheManifest.json`** is a generated build artifact (gitignored, produced by `gulpfile.js/index.js`). It lists files + hashes for the service-worker cache. Never hand-edit or commit it — it is regenerated by the build, so edits are overwritten and won't be tracked anyway. When you add/remove/rename source files, just let the build regenerate it.

## Translations / i18n
- All user-visible strings must go in `src/nls/root/strings.js` — never hardcode English in source files.
- All user-visible strings must go in `src/nls/root/strings.js` — never hardcode English in source files. This applies only to genuinely translatable natural-language text. Content that must render identically in every locale — literal code syntax, keyword/identifier examples, brand names — is not translatable and must NOT go in strings.js; keep it as a local constant in the source file instead. Reason: `src/nls/root/strings.js` values are sent as-is to an automated AI translation pass (`gulpfile.js/translateStrings.js`) with no awareness that a given string represents code rather than prose, so a translatable-looking word embedded in code syntax (e.g. `name` in `function name() {...}`) can get mistranslated into garbled pseudo-code in other locales.
- Use `const Strings = require("strings");` then `Strings.KEY_NAME`.
- For parameterized strings use `StringUtils.format(Strings.KEY, arg0, arg1)` with `{0}`, `{1}` placeholders.
- Keys use UPPER_SNAKE_CASE grouped by feature prefix (e.g. `AI_CHAT_*`).
Expand Down
33 changes: 33 additions & 0 deletions src/editor/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,37 @@
};
}

/**
* Mark option for a subdued outline box, used to show every remaining stop of an active
* snippet/tab-stop session (see editor/TabstopManager.js) so the user can see at a glance how
* many fields are left and where, even for the ones they haven't tabbed to yet.
*/
function getMarkOptionTabstopOutline() {

Check warning on line 1404 in src/editor/Editor.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function 'getMarkOptionTabstopOutline' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaAGkC7GyDvelYErxKmf&open=AaAGkC7GyDvelYErxKmf&pullRequest=3095
return {
className: "editor-text-tabstop-outline",
startStyle: "editor-text-tabstop-outline-left",
endStyle: "editor-text-tabstop-outline-right",
clearWhenEmpty: false,
inclusiveLeft: true,
inclusiveRight: true
};
}

/**
* Mark option for the bold/active variant of the above, layered on top of it for whichever stop
* is currently selected in an active snippet/tab-stop session.
*/
function getMarkOptionTabstopOutlineActive() {

Check warning on line 1419 in src/editor/Editor.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function 'getMarkOptionTabstopOutlineActive' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaAGkC7GyDvelYErxKmg&open=AaAGkC7GyDvelYErxKmg&pullRequest=3095
return {
className: "editor-text-tabstop-outline-active",
startStyle: "editor-text-tabstop-outline-active-left",
endStyle: "editor-text-tabstop-outline-active-right",
clearWhenEmpty: false,
inclusiveLeft: true,
inclusiveRight: true
};
}

/**
* Mark option to underline errors.
*/
Expand Down Expand Up @@ -1430,6 +1461,8 @@
* Mark option for renaming outlines.
*/
Editor.getMarkOptionRenameOutline = getMarkOptionRenameOutline;
Editor.getMarkOptionTabstopOutline = getMarkOptionTabstopOutline;
Editor.getMarkOptionTabstopOutlineActive = getMarkOptionTabstopOutlineActive;

/**
* Can be used to mark a range of text with a specific CSS class name. cursorFrom and cursorTo should be {line, ch}
Expand Down
98 changes: 90 additions & 8 deletions src/editor/TabstopManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,19 @@
* stop, and (when there is more than one stop) starts a Tab-navigable session backed by markers
* so the stops follow any later edits (e.g. an auto-import line inserted above).
*
* NOTE: this is currently wired only into the LSP completion path (languageTools/DefaultProviders).
* The Emmet expander (HTMLCodeHints) and the custom-snippets feature have their own stable cursor
* handling and were intentionally left untouched; they can migrate onto this manager in future.
* Used by the LSP completion path (languageTools/DefaultProviders), DocCommentHints, and Custom
* Snippets (extensionsIntegrated/CustomSnippets/snippetCursorManager.js). The Emmet expander
* (HTMLCodeHints) still has its own separate cursor handling.
*
* While a Tab-navigable session is active, every remaining stop gets a subdued outline box (see
* Editor.getMarkOptionTabstopOutline) so the user can see at a glance how many fields are left and
* where, with the currently-selected one getting a bolder "active" outline layered on top (see
* Editor.getMarkOptionTabstopOutlineActive) - matches the visual language RenameIdentifier.js already
* uses for its own outline box. Zero-width stops (a bare `$N`/`$0` with no default text - just a
* caret position, no marker range) don't get an outline, since there's no span to box.
*/
define(function (require, exports, module) {
const Editor = require("editor/Editor").Editor;

/**
* Expand an LSP snippet into plain text plus the list of tab-stops.
Expand Down Expand Up @@ -174,7 +182,7 @@ define(function (require, exports, module) {

// ---- Tab-navigation session ----------------------------------------------------------------

var _session = null; // { editor, markers: [marker], index, keymap }
var _session = null; // { editor, markers: [marker], index, keymap, activeOutlineMarker }

function _clearSession() {
if (!_session) {
Expand All @@ -185,10 +193,49 @@ define(function (require, exports, module) {
session.markers.forEach(function (m) {
m.clear();
});
if (session.activeOutlineMarker) {
session.activeOutlineMarker.clear();
}
session.editor._codeMirror.removeKeyMap(session.keymap);
session.editor.off(".tabstop");
}

/**
* @param {{line: number, ch: number}} pos - a document position
* @return {boolean} true if `pos` falls within the line span currently covered by the active
* session's markers (i.e. the snippet the user is still tabbing through)
*/
function _isWithinSessionBounds(pos) {
var minLine = Infinity,
maxLine = -Infinity;
_session.markers.forEach(function (m) {
var r = _markerRange(m);
if (r) {
minLine = Math.min(minLine, r.from.line);
maxLine = Math.max(maxLine, r.to.line);
}
});
if (minLine === Infinity) {
return false; // no markers left resolve-able
}
return pos.line >= minLine && pos.line <= maxLine;
}

/**
* Ends the session as soon as the user's cursor leaves the snippet's lines (e.g. clicks
* elsewhere to fix something unrelated) or a multi-cursor selection is made - matches standard
* editor behavior (VS Code et al.) and avoids a stray later Tab press unexpectedly jumping the
* cursor back into a snippet the user has moved on from.
*/
function _handleCursorActivity(event, editor) {
if (!_session || _session.editor !== editor) {
return;
}
if (editor.getSelections().length > 1 || !_isWithinSessionBounds(editor.getCursorPos())) {
_clearSession();
}
}

/**
* Resolve a marker (markText range or bookmark) to a {from, to} document range, or null if the
* marker no longer exists in the document.
Expand All @@ -215,10 +262,28 @@ define(function (require, exports, module) {
}
_session.index = index;
_session.editor.setSelection(range.from, range.to);

// swap the bold "active" outline onto whichever stop we just landed on - only meaningful for
// a real span (a bare $N/$0 with no default text is a zero-width caret, nothing to box)
if (_session.activeOutlineMarker) {
_session.activeOutlineMarker.clear();
_session.activeOutlineMarker = null;
}
if (range.from.line !== range.to.line || range.from.ch !== range.to.ch) {
_session.activeOutlineMarker = _session.editor.markText(
"tabstop-active", range.from, range.to, Editor.getMarkOptionTabstopOutlineActive());
}
return true;
}

function _gotoNext() {
if (!_session) {
// no-op: goToNextStop/goToPreviousStop are exported as public API (see bottom of file)
// for callers like Custom Snippets' snippetCursorManager.js to drive navigation directly,
// not only via the CodeMirror keymap installed below (which only exists while a session is
// active, so it could never reach this function with no session) - a direct caller could.
return;
}
// Move forward through the stops; leaving the last one ends the session (caret stays put).
for (var i = _session.index + 1; i < _session.markers.length; i++) {
if (_selectStop(i)) {
Expand All @@ -233,6 +298,9 @@ define(function (require, exports, module) {
}

function _gotoPrev() {
if (!_session) {
return; // see _gotoNext's no-op comment - same reasoning applies here
}
for (var i = _session.index - 1; i >= 0; i--) {
if (_selectStop(i)) {
return;
Expand Down Expand Up @@ -286,6 +354,11 @@ define(function (require, exports, module) {
}

// Multiple stops: lay down markers and start a Tab-navigable session.
// the subdued outline (visual only) is layered onto the SAME functional tracking options
// below by className/startStyle/endStyle alone - deliberately not spreading the whole helper
// object in, since its own inclusiveLeft/clearWhenEmpty differ from what marker TRACKING here
// actually needs (inclusiveLeft: false is what makes typing at a stop's start not stick to it).
var outlineOption = Editor.getMarkOptionTabstopOutline();
var markers = parsed.stops.map(function (stop) {
var ms = posFromOffset(stop.start),
me = posFromOffset(stop.end);
Expand All @@ -295,7 +368,10 @@ define(function (require, exports, module) {
return editor.markText("tabstop", ms, me, {
clearWhenEmpty: false,
inclusiveLeft: false,
inclusiveRight: true
inclusiveRight: true,
className: outlineOption.className,
startStyle: outlineOption.startStyle,
endStyle: outlineOption.endStyle
});
});

Expand All @@ -312,11 +388,13 @@ define(function (require, exports, module) {
}
};

_session = { editor: editor, markers: markers, index: -1, keymap: keymap };
_session = { editor: editor, markers: markers, index: -1, keymap: keymap, activeOutlineMarker: null };
editor._codeMirror.addKeyMap(keymap);
// End the session if the editor it belongs to is destroyed (file closed). Namespaced so
// _clearSession can remove it with a single off(".tabstop").
// End the session if the editor it belongs to is destroyed (file closed), or the user moves
// on (cursor leaves the snippet's lines, or a multi-cursor selection is made). Namespaced so
// _clearSession can remove both with a single off(".tabstop").
editor.on("beforeDestroy.tabstop", _clearSession);
editor.on("cursorActivity.tabstop", _handleCursorActivity);

_selectStop(0);
return parsed;
Expand All @@ -338,4 +416,8 @@ define(function (require, exports, module) {
exports.insertSnippet = insertSnippet;
exports.hasActiveSession = hasActiveSession;
exports.endSession = endSession;
// exposed so other features with their own stable session lifecycle (e.g. custom snippets)
// can drive Tab / Shift-Tab navigation without duplicating this logic
exports.goToNextStop = _gotoNext;
exports.goToPreviousStop = _gotoPrev;
});
57 changes: 28 additions & 29 deletions src/extensionsIntegrated/CustomSnippets/codeHintIntegration.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ define(function (require, exports, module) {
const EditorManager = require("editor/EditorManager");
const Metrics = require("utils/Metrics");

const Global = require("./global");
const Driver = require("./driver");
const Helper = require("./helper");
const SnippetCursorManager = require("./snippetCursorManager");
Expand Down Expand Up @@ -84,7 +83,8 @@ define(function (require, exports, module) {

if (matchingSnippets.length > 0) {
const customSnippetHints = matchingSnippets.map((snippet) => {
return Helper.createHintItem(snippet.abbreviation, needle.word, snippet.description);
return Helper.createHintItem(
snippet.abbreviation, needle.word, snippet.description, snippet.insertionKey);
});

return {
Expand All @@ -108,34 +108,33 @@ define(function (require, exports, module) {
insertHint: function (hint) {
// check if the hint is a custom snippet
if (hint && hint.jquery && hint.attr("data-isCustomSnippet")) {
// handle custom snippet insertion
const abbreviation = hint.attr("data-val");
if (Global.SnippetHintsList) {
const matchedSnippet = Global.SnippetHintsList.find(
(snippet) => snippet.abbreviation === abbreviation
);
// handle custom snippet insertion. The hint list was already built from the correctly
// language-scoped candidates (see getHints above), and each hint element carries the
// exact resolved snippet's insertionKey - so accepting it is a direct O(1) lookup, not
// a re-search by abbreviation + the (possibly since-changed) current language context
const insertionKey = hint.attr("data-insertion-key");

// Get current editor from EditorManager since it's not passed
const editor = EditorManager.getActiveEditor();
if (editor) {
const matchedSnippet = Helper.getSnippetByInsertionKey(insertionKey);
if (matchedSnippet) {
// Get current editor from EditorManager since it's not passed
const editor = EditorManager.getActiveEditor();

if (editor) {
// to track the usage metrics
const fileCategory = Helper.categorizeFileExtensionForMetrics(matchedSnippet.fileExtension);
Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "snipt", `use.${fileCategory}`);

// replace the typed abbreviation with the template text using cursor manager
const wordInfo = Driver.getWordBeforeCursor();
const start = { line: wordInfo.line, ch: wordInfo.ch + 1 };
const end = editor.getCursorPos();

SnippetCursorManager.insertSnippetWithTabStops(
editor,
matchedSnippet.templateText,
start,
end
);
return true; // handled
}
// to track the usage metrics
const fileCategory = Helper.categorizeFileExtensionForMetrics(matchedSnippet.fileExtension);
Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "snipt", `use.${fileCategory}`);

// replace the typed abbreviation with the template text using cursor manager
const wordInfo = Driver.getWordBeforeCursor();
const start = { line: wordInfo.line, ch: wordInfo.ch + 1 };
const end = editor.getCursorPos();

SnippetCursorManager.insertSnippetWithTabStops(
editor,
matchedSnippet.templateText,
start,
end
);
return true; // handled
}
}
}
Expand Down
Loading
Loading