Compare commits

...
Author SHA1 Message Date
l0ng-ai 6b3519c7f8 fix(python): refresh the interpreter page after recreating an environment, pick a conda env's Python version
Creating an environment at the path the project interpreter already pointed to (a deleted .venv recreated) left the Python Interpreter page saying "<path> does not exist": the package list and the validity check only reloaded when the path changed. Both now reload after an environment is created.

A new conda environment offered a "Base interpreter" chosen among existing environments, which read as copying one; only its Python version was used. It now asks for a Python version (conda create -p <location> python=<version>), and the note says the environment starts with only Python.

Refs #3109
2026-11-13 18:55:24 +08:00
l0ng-ai fdc8bc2d4c fix(editor): leave reference counts to Code Vision's usages (#3139)
csharp-ls adds a "3 Reference(s)" lens above each declaration, next to Code Vision's own "3 usages". It duplicated the count, ignored the Code Vision settings, and was a second set of lens rows replaced after every edit. Language-server lenses that are only a reference count are now left out; Settings › Editor › Inlay Hints › Code Vision › Usages governs the count.

Refs #3107
2026-11-13 18:41:34 +08:00
l0ng-ai 497d4766d7 fix(vcs): lay out a merge commit's file side by side, each pane scrolling (#3138)
Opening a file of a merge commit from the Git log stacked its three panes in a column the editor could not scroll: only the first parent's text showed, without the changes, and neither the wheel nor the arrow keys moved it. The panes now sit side by side under a label row, each scrolls and they stay in step.

Fixes #3108
2026-11-13 18:38:56 +08:00
7 changed files with 128 additions and 30 deletions

No files matched your search

+24 -2
View File
@@ -46,7 +46,7 @@ it("keeps mapped lenses across edits instead of clearing them (#2710)", async ()
data: [
{
range: { start: { line: 1, col: 0 }, end: { line: 1, col: 3 } },
command: { title: "1 reference", command: "csharp.showReferences", arguments: [] },
command: { title: "Run test", command: "csharp.runTest", arguments: [] },
},
],
});
@@ -67,5 +67,27 @@ it("keeps mapped lenses across edits instead of clearing them (#2710)", async ()
const after = view.state.facet(codeLenses);
expect(after).toHaveLength(1);
expect(after[0]!.pos).toBe(posBefore + 1);
expect(after[0]!.title).toBe("1 reference");
expect(after[0]!.title).toBe("Run test");
});
it("leaves reference counts to Code Vision's usages", async () => {
vi.useFakeTimers();
const lens = (line: number, title: string) => ({
range: { start: { line, col: 0 }, end: { line, col: 1 } },
command: { title, command: "csharp.showReferences", arguments: [] },
});
ipc.lspCodeLens.mockResolvedValue({
status: "ok",
data: [lens(0, "3 Reference(s)"), lens(1, "1 reference"), lens(2, "12 references"), lens(3, "2 implementations")],
});
const view = new EditorView({
state: EditorState.create({
doc: "a\nb\nc\nd",
extensions: serverCodeLenses({ doc: 1, path: "/p/a.cs", language: "csharp", uiMode: null, readOnly: false }),
}),
parent: document.body,
});
views.push(view);
await vi.advanceTimersByTimeAsync(400);
expect(view.state.facet(codeLenses).map((l) => l.title)).toEqual(["2 implementations"]);
});
+7 -1
View File
@@ -6,6 +6,12 @@ import { call, commands } from "@/platform/ipc";
import { onNavigationProvidersChanged } from "@/editor/navigationProviders";
import { posToOffset } from "./positions";
import { onLspRefresh, onServerSettled } from "./availability";
/**
* A server's reference count ("3 references", csharp-ls's "3 Reference(s)"): Code Vision's
* Usages shows the same count above the declaration, under its own setting.
*/
const REFERENCE_COUNT = /^\s*\d+\s+references?(\(s\))?\s*$/i;
const set = StateEffect.define<CodeLens[]>();
const field = StateField.define<CodeLens[]>({
create: () => [],
@@ -62,7 +68,7 @@ export function serverCodeLenses(ctx: EditorDocContext): Extension {
view.dispatch({
effects: set.of(
result.flatMap((l) =>
l.command
l.command && !REFERENCE_COUNT.test(l.command.title)
? [
{
pos: posToOffset(text, l.range.start),
@@ -43,13 +43,23 @@ export function AddInterpreterDialog({
const [error, setError] = useState<string | null>(null);
const ids = { base: useId(), location: useId(), existing: useId() };
const baseValue = base ?? bases[0]?.path ?? null;
const toOption = (s: { path: string; name: string }): SelectOption => ({
value: s.path,
label: s.name,
detail: tildify(s.path),
});
const baseOptions = bases.map(toOption);
// conda installs a fresh Python of the base's version: offer versions, not environments to copy.
const versionOptions = (() => {
const seen = new Map<string, SelectOption>();
for (const s of bases) {
const short = s.version?.split(".").slice(0, 2).join(".");
if (short && !seen.has(short)) seen.set(short, { value: s.path, label: tx`Python ${short}` });
}
return [...seen.values()].sort((a, b) => b.label.localeCompare(a.label, undefined, { numeric: true }));
})();
const baseOptions = kind === "conda" ? versionOptions : bases.map(toOption);
const baseValue =
base ?? (kind === "conda" ? (versionOptions[0]?.value as string | undefined) : bases[0]?.path) ?? null;
const existingOptions = [...browsed, ...list.filter((s) => !browsed.some((b) => b.path === s.path))].map((s) => ({
value: s.path,
label: `${s.name} — ${tildify(s.path)}`,
@@ -108,7 +118,9 @@ export function AddInterpreterDialog({
const canOk = !busy && (mode === "new" ? !!baseValue && !!location.trim() : !!existing?.trim());
const note = {
venv: tx("Runs “<base> -m venv <location>”; the new interpreter becomes the project interpreter."),
conda: tx("Runs “conda create -p <location>”; the new interpreter becomes the project interpreter."),
conda: tx(
"Runs “conda create -p <location> python=<version>”: a new environment with only Python in it; the new interpreter becomes the project interpreter.",
),
uv: tx("Runs “uv venv <location> --python <base>”; the new interpreter becomes the project interpreter."),
poetry: tx(
"Runs “poetry env use <base>” in the project; poetry keeps the environment under its own cache directory.",
@@ -143,6 +155,7 @@ export function AddInterpreterDialog({
aria-current={kind === k.tool || undefined}
onClick={() => {
setKind(k.tool);
setBase(null);
setError(null);
}}
>
@@ -199,9 +212,7 @@ export function AddInterpreterDialog({
}}
/>
</div>
<label htmlFor={ids.base}>
<T>Base interpreter:</T>
</label>
<label htmlFor={ids.base}>{kind === "conda" ? <T>Python version:</T> : <T>Base interpreter:</T>}</label>
<div className={styles.pathRow}>
<Select
className={styles.pathInput}
@@ -209,17 +220,19 @@ export function AddInterpreterDialog({
options={baseOptions}
onChange={setBase}
placeholder={loading ? "Detecting…" : "No Python found"}
aria-label="Base interpreter"
/>
<IconButton
icon="folder-open"
size="small"
title="Browse…"
onClick={async () => {
const p = await choosePath({ directory: false, title: "Select Base Interpreter" });
if (p) setBase(p);
}}
aria-label={kind === "conda" ? "Python version" : "Base interpreter"}
/>
{kind !== "conda" && (
<IconButton
icon="folder-open"
size="small"
title="Browse…"
onClick={async () => {
const p = await choosePath({ directory: false, title: "Select Base Interpreter" });
if (p) setBase(p);
}}
/>
)}
</div>
<div className={styles.note}>{note}</div>
</>
@@ -68,6 +68,8 @@ export function PythonInterpreterPage() {
const [activate, setActivate] = useSetting<boolean>("languages.python.activateVirtualenv");
const root = useProject((s) => s.project?.root ?? null);
const [auto, setAuto] = useState<string | null>(null);
// Bumped when an environment is created: it may reuse the configured path, whose state changed.
const [created, setCreated] = useState(0);
// Detected interpreters plus the ones added to (or hidden from) the IDE-wide list.
const { detected, rows, hidden } = usePythonInterpreters({ withProjects: false });
const extras = useMemo(
@@ -91,7 +93,7 @@ export function PythonInterpreterPage() {
return () => {
live = false;
};
}, [root]);
}, [root, created]);
// The interpreter runs resolve to: the configured one while it exists, else the fallback.
const [resolved, setResolved] = useState<{ interpreter: string; path: string | null } | null>(null);
@@ -104,7 +106,7 @@ export function PythonInterpreterPage() {
return () => {
live = false;
};
}, [root, interpreter]);
}, [root, interpreter, created]);
// A configured interpreter whose executable is gone (PyCharm's "Invalid"): runs fall back.
const invalid =
!!interpreter && resolved?.interpreter === interpreter && !sameInterpreter(resolved.path, interpreter, root);
@@ -362,6 +364,8 @@ export function PythonInterpreterPage() {
onClose={() => setAdding(false)}
onAdded={(path) => {
setInterpreter(path);
setCreated((n) => n + 1);
reload();
setAdding(false);
}}
/>
+48
View File
@@ -444,6 +444,54 @@
display: flex;
flex-direction: column;
}
/* Go to a merge commit's file: parent 1 | merge result | parent 2, each pane scrolling itself. */
.mergeDiff {
flex: 1;
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
.mergeDiffLabels {
flex: none;
height: var(--h-row);
display: grid;
grid-template-columns: 1fr 1fr 1fr;
border-bottom: 1px solid var(--border);
color: var(--fg-secondary);
font-size: var(--fs-small);
}
.mergeDiffLabels > span {
display: flex;
align-items: center;
min-width: 0;
padding: 0 12px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mergeDiffLabels > span + span {
border-left: 1px solid var(--border);
}
.mergeDiffPanes {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.mergeDiffPane {
min-width: 0;
min-height: 0;
display: flex;
overflow: hidden;
}
.mergeDiffPane + .mergeDiffPane {
border-left: 1px solid var(--border);
}
.mergeDiffPane > :global(.cm-editor) {
flex: 1;
min-width: 0;
}
.subjectLine {
color: var(--fg-strong);
font-weight: 600;
+11 -8
View File
@@ -51,10 +51,7 @@ export function GitMergeDiff({ input }: { input: EditorInput }) {
}
for (const [i, text] of texts.entries()) {
const pane = document.createElement("div");
pane.style.cssText = "flex:1;min-width:0;display:flex;flex-direction:column";
const label = document.createElement("div");
label.textContent = `${sides[i]!.rev.slice(0, 8)} · ${sides[i]!.path}`;
pane.append(label);
pane.className = styles.mergeDiffPane!;
host.current.append(pane);
const view = new EditorView({
parent: pane,
@@ -86,10 +83,16 @@ export function GitMergeDiff({ input }: { input: EditorInput }) {
element?.replaceChildren();
};
}, [data]);
const labels = [data.parents[0]!, { rev: data.revision, path: data.path }, data.parents[1]!];
return (
<>
<div role={error ? "alert" : undefined}>{error}</div>
<div className={styles.revisionSource} style={{ display: "flex" }} ref={host} />
</>
<div className={styles.mergeDiff}>
{error && <div role="alert">{error}</div>}
<div className={styles.mergeDiffLabels}>
{labels.map((side, i) => (
<span key={i}>{`${side.rev.slice(0, 8)} · ${side.path}`}</span>
))}
</div>
<div className={styles.mergeDiffPanes} ref={host} />
</div>
);
}
+3 -1
View File
@@ -5217,7 +5217,9 @@
"{count} modified action": "已修改 {count} 个操作",
"{count} modified actions": "已修改 {count} 个操作",
"Runs “<base> -m venv <location>”; the new interpreter becomes the project interpreter.": "运行“<base> -m venv <location>”;新的解释器将成为项目解释器。",
"Runs “conda create -p <location>”; the new interpreter becomes the project interpreter.": "运行“conda create -p <location>”;新的解释器将成为项目解释器。",
"Runs “conda create -p <location> python=<version>”: a new environment with only Python in it; the new interpreter becomes the project interpreter.": "运行“conda create -p <location> python=<version>”:新建一个只包含 Python 的环境;新的解释器将成为项目解释器。",
"Python version": "Python 版本",
"Python {short}": "Python {short}",
"Runs “poetry env use <base>” in the project; poetry keeps the environment under its own cache directory.": "在项目中运行“poetry env use <base>”;Poetry 会将环境保存在其自己的缓存目录下。",
"Runs “uv venv <location> --python <base>”; the new interpreter becomes the project interpreter.": "运行“uv venv <location> --python <base>”;新的解释器将成为项目解释器。",
"{vendor} {version} is unpacked into a folder of its own in {location}, where JDK detection finds it.": "{vendor} {version} 将解压到 {location} 下它自己的文件夹中,JDK 检测会在那里找到它。",