mirror of https://github.com/astral-sh/ruff
[ty] Support stdlib files in playground (#19557)
This commit is contained in:
parent
738246627f
commit
469c50b0b7
|
|
@ -3,21 +3,23 @@ use std::any::Any;
|
|||
use js_sys::{Error, JsString};
|
||||
use ruff_db::Db as _;
|
||||
use ruff_db::diagnostic::{self, DisplayDiagnosticConfig};
|
||||
use ruff_db::files::{File, FileRange, system_path_to_file};
|
||||
use ruff_db::files::{File, FilePath, FileRange, system_path_to_file, vendored_path_to_file};
|
||||
use ruff_db::source::{SourceText, line_index, source_text};
|
||||
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
|
||||
use ruff_db::system::{
|
||||
CaseSensitivity, DirectoryEntry, GlobError, MemoryFileSystem, Metadata, PatternError, System,
|
||||
SystemPath, SystemPathBuf, SystemVirtualPath, WritableSystem,
|
||||
};
|
||||
use ruff_db::vendored::VendoredPath;
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_formatter::formatted_file;
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceLocation};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ty_ide::{
|
||||
MarkupKind, NavigationTargets, RangedValue, document_highlights, goto_declaration,
|
||||
goto_definition, goto_references, goto_type_definition, hover, inlay_hints, signature_help,
|
||||
MarkupKind, RangedValue, document_highlights, goto_declaration, goto_definition,
|
||||
goto_references, goto_type_definition, hover, inlay_hints,
|
||||
};
|
||||
use ty_ide::{NavigationTargets, signature_help};
|
||||
use ty_project::metadata::options::Options;
|
||||
use ty_project::metadata::value::ValueSource;
|
||||
use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
|
||||
|
|
@ -158,28 +160,35 @@ impl Workspace {
|
|||
|
||||
self.db.project().open_file(&mut self.db, file);
|
||||
|
||||
Ok(FileHandle { path, file })
|
||||
Ok(FileHandle {
|
||||
path: path.into(),
|
||||
file,
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "updateFile")]
|
||||
pub fn update_file(&mut self, file_id: &FileHandle, contents: &str) -> Result<(), Error> {
|
||||
if !self.system.fs.exists(&file_id.path) {
|
||||
let system_path = file_id.path.as_system_path().ok_or_else(|| {
|
||||
Error::new("Cannot update non-system files (vendored files are read-only)")
|
||||
})?;
|
||||
|
||||
if !self.system.fs.exists(system_path) {
|
||||
return Err(Error::new("File does not exist"));
|
||||
}
|
||||
|
||||
self.system
|
||||
.fs
|
||||
.write_file(&file_id.path, contents)
|
||||
.write_file(system_path, contents)
|
||||
.map_err(into_error)?;
|
||||
|
||||
self.db.apply_changes(
|
||||
vec![
|
||||
ChangeEvent::Changed {
|
||||
path: file_id.path.to_path_buf(),
|
||||
path: system_path.to_path_buf(),
|
||||
kind: ChangedKind::FileContent,
|
||||
},
|
||||
ChangeEvent::Changed {
|
||||
path: file_id.path.to_path_buf(),
|
||||
path: system_path.to_path_buf(),
|
||||
kind: ChangedKind::FileMetadata,
|
||||
},
|
||||
],
|
||||
|
|
@ -198,18 +207,22 @@ impl Workspace {
|
|||
let file = file_id.file;
|
||||
|
||||
self.db.project().close_file(&mut self.db, file);
|
||||
self.system
|
||||
.fs
|
||||
.remove_file(&file_id.path)
|
||||
.map_err(into_error)?;
|
||||
|
||||
self.db.apply_changes(
|
||||
vec![ChangeEvent::Deleted {
|
||||
path: file_id.path.to_path_buf(),
|
||||
kind: DeletedKind::File,
|
||||
}],
|
||||
None,
|
||||
);
|
||||
// Only close system files (vendored files can't be closed/deleted)
|
||||
if let Some(system_path) = file_id.path.as_system_path() {
|
||||
self.system
|
||||
.fs
|
||||
.remove_file(system_path)
|
||||
.map_err(into_error)?;
|
||||
|
||||
self.db.apply_changes(
|
||||
vec![ChangeEvent::Deleted {
|
||||
path: system_path.to_path_buf(),
|
||||
kind: DeletedKind::File,
|
||||
}],
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -556,6 +569,22 @@ impl Workspace {
|
|||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Gets a file handle for a vendored file by its path.
|
||||
/// This allows vendored files to participate in LSP features like hover, completions, etc.
|
||||
#[wasm_bindgen(js_name = "getVendoredFile")]
|
||||
pub fn get_vendored_file(&self, path: &str) -> Result<FileHandle, Error> {
|
||||
let vendored_path = VendoredPath::new(path);
|
||||
|
||||
// Try to get the vendored file as a File
|
||||
let file = vendored_path_to_file(&self.db, vendored_path)
|
||||
.map_err(|err| Error::new(&format!("Vendored file not found: {path}: {err}")))?;
|
||||
|
||||
Ok(FileHandle {
|
||||
file,
|
||||
path: vendored_path.to_path_buf().into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_error<E: std::fmt::Display>(err: E) -> Error {
|
||||
|
|
@ -598,7 +627,7 @@ fn map_targets_to_links(
|
|||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[wasm_bindgen(inspectable)]
|
||||
pub struct FileHandle {
|
||||
path: SystemPathBuf,
|
||||
path: FilePath,
|
||||
file: File,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
Theme,
|
||||
VerticalResizeHandle,
|
||||
} from "shared";
|
||||
import type { Workspace } from "ty_wasm";
|
||||
import { FileHandle, Workspace } from "ty_wasm";
|
||||
import { Panel, PanelGroup } from "react-resizable-panels";
|
||||
import { Files, isPythonFile } from "./Files";
|
||||
import SecondarySideBar from "./SecondarySideBar";
|
||||
|
|
@ -22,6 +22,7 @@ import SecondaryPanel, {
|
|||
SecondaryTool,
|
||||
} from "./SecondaryPanel";
|
||||
import Diagnostics, { Diagnostic } from "./Diagnostics";
|
||||
import VendoredFileBanner from "./VendoredFileBanner";
|
||||
import { FileId, ReadonlyFiles } from "../Playground";
|
||||
import type { editor } from "monaco-editor";
|
||||
import type { Monaco } from "@monaco-editor/react";
|
||||
|
|
@ -49,6 +50,10 @@ export interface Props {
|
|||
onRemoveFile(workspace: Workspace, file: FileId): void;
|
||||
|
||||
onSelectFile(id: FileId): void;
|
||||
|
||||
onSelectVendoredFile(handle: FileHandle): void;
|
||||
|
||||
onClearVendoredFile(): void;
|
||||
}
|
||||
|
||||
export default function Chrome({
|
||||
|
|
@ -61,6 +66,8 @@ export default function Chrome({
|
|||
onRemoveFile,
|
||||
onSelectFile,
|
||||
onChangeFile,
|
||||
onSelectVendoredFile,
|
||||
onClearVendoredFile,
|
||||
}: Props) {
|
||||
const workspace = use(workspacePromise);
|
||||
|
||||
|
|
@ -78,6 +85,24 @@ export default function Chrome({
|
|||
editorRef.current?.editor.focus();
|
||||
};
|
||||
|
||||
const handleBackToUserFile = useCallback(() => {
|
||||
if (editorRef.current && files.selected != null) {
|
||||
const selectedFile = files.index.find(
|
||||
(file) => file.id === files.selected,
|
||||
);
|
||||
if (selectedFile != null) {
|
||||
const monaco = editorRef.current.monaco;
|
||||
const fileUri = monaco.Uri.file(selectedFile.name);
|
||||
const userModel = monaco.editor.getModel(fileUri);
|
||||
|
||||
if (userModel != null) {
|
||||
onClearVendoredFile();
|
||||
editorRef.current.editor.setModel(userModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [files.selected, files.index, onClearVendoredFile]);
|
||||
|
||||
const handleSecondaryToolSelected = useCallback(
|
||||
(tool: SecondaryTool | null) => {
|
||||
setSecondaryTool((secondaryTool) => {
|
||||
|
|
@ -134,7 +159,12 @@ export default function Chrome({
|
|||
[workspace, files.index, onRemoveFile],
|
||||
);
|
||||
|
||||
const checkResult = useCheckResult(files, workspace, secondaryTool);
|
||||
const checkResult = useCheckResult(
|
||||
files,
|
||||
workspace,
|
||||
secondaryTool,
|
||||
files.currentVendoredFile ?? null,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -153,11 +183,25 @@ export default function Chrome({
|
|||
<Panel
|
||||
id="main"
|
||||
order={0}
|
||||
className="flex flex-col gap-2 my-4"
|
||||
className={`flex flex-col gap-2 ${files.currentVendoredFile ? "mb-4" : "my-4"}`}
|
||||
minSize={10}
|
||||
>
|
||||
<PanelGroup id="vertical" direction="vertical">
|
||||
<Panel minSize={10} className="my-2" order={0}>
|
||||
<Panel
|
||||
minSize={10}
|
||||
className={files.currentVendoredFile ? "mb-2" : "my-2"}
|
||||
order={0}
|
||||
>
|
||||
{files.currentVendoredFile != null && (
|
||||
<VendoredFileBanner
|
||||
currentVendoredFile={files.currentVendoredFile}
|
||||
selectedFile={{
|
||||
id: files.selected,
|
||||
name: selectedFileName,
|
||||
}}
|
||||
onBackToUserFile={handleBackToUserFile}
|
||||
/>
|
||||
)}
|
||||
<Editor
|
||||
theme={theme}
|
||||
visible={true}
|
||||
|
|
@ -169,6 +213,9 @@ export default function Chrome({
|
|||
onMount={handleEditorMount}
|
||||
onChange={(content) => onChangeFile(workspace, content)}
|
||||
onOpenFile={onSelectFile}
|
||||
onVendoredFileChange={onSelectVendoredFile}
|
||||
onBackToUserFile={handleBackToUserFile}
|
||||
isViewingVendoredFile={files.currentVendoredFile != null}
|
||||
/>
|
||||
{checkResult.error ? (
|
||||
<div
|
||||
|
|
@ -182,8 +229,8 @@ export default function Chrome({
|
|||
<ErrorMessage>{checkResult.error}</ErrorMessage>
|
||||
</div>
|
||||
) : null}
|
||||
<VerticalResizeHandle />
|
||||
</Panel>
|
||||
<VerticalResizeHandle />
|
||||
<Panel
|
||||
id="diagnostics"
|
||||
minSize={3}
|
||||
|
|
@ -231,22 +278,26 @@ function useCheckResult(
|
|||
files: ReadonlyFiles,
|
||||
workspace: Workspace,
|
||||
secondaryTool: SecondaryTool | null,
|
||||
currentVendoredFileHandle: FileHandle | null,
|
||||
): CheckResult {
|
||||
const deferredContent = useDeferredValue(
|
||||
files.selected == null ? null : files.contents[files.selected],
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
if (files.selected == null || deferredContent == null) {
|
||||
return {
|
||||
diagnostics: [],
|
||||
error: null,
|
||||
secondary: null,
|
||||
};
|
||||
}
|
||||
// Determine which file handle to use
|
||||
const currentHandle =
|
||||
currentVendoredFileHandle ??
|
||||
(files.selected == null ? null : files.handles[files.selected]);
|
||||
|
||||
const currentHandle = files.handles[files.selected];
|
||||
if (currentHandle == null || !isPythonFile(currentHandle)) {
|
||||
const isVendoredFile = currentVendoredFileHandle != null;
|
||||
|
||||
// Regular file handling
|
||||
if (
|
||||
currentHandle == null ||
|
||||
deferredContent == null ||
|
||||
!isPythonFile(currentHandle)
|
||||
) {
|
||||
return {
|
||||
diagnostics: [],
|
||||
error: null,
|
||||
|
|
@ -255,7 +306,10 @@ function useCheckResult(
|
|||
}
|
||||
|
||||
try {
|
||||
const diagnostics = workspace.checkFile(currentHandle);
|
||||
// Don't run diagnostics for vendored files - always empty
|
||||
const diagnostics = isVendoredFile
|
||||
? []
|
||||
: workspace.checkFile(currentHandle);
|
||||
|
||||
let secondary: SecondaryPanelResult = null;
|
||||
|
||||
|
|
@ -276,10 +330,15 @@ function useCheckResult(
|
|||
break;
|
||||
|
||||
case "Run":
|
||||
secondary = {
|
||||
status: "ok",
|
||||
content: "",
|
||||
};
|
||||
secondary = isVendoredFile
|
||||
? {
|
||||
status: "error",
|
||||
error: "Cannot run vendored/standard library files",
|
||||
}
|
||||
: {
|
||||
status: "ok",
|
||||
content: "",
|
||||
};
|
||||
break;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
|
|
@ -289,8 +348,7 @@ function useCheckResult(
|
|||
};
|
||||
}
|
||||
|
||||
// Eagerly convert the diagnostic to avoid out of bound errors
|
||||
// when the diagnostics are "deferred".
|
||||
// Convert diagnostics (empty array for vendored files)
|
||||
const serializedDiagnostics = diagnostics.map((diagnostic) => ({
|
||||
id: diagnostic.id(),
|
||||
message: diagnostic.message(),
|
||||
|
|
@ -305,9 +363,6 @@ function useCheckResult(
|
|||
secondary,
|
||||
};
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
|
||||
return {
|
||||
diagnostics: [],
|
||||
error: formatError(e),
|
||||
|
|
@ -320,6 +375,7 @@ function useCheckResult(
|
|||
files.selected,
|
||||
files.handles,
|
||||
secondaryTool,
|
||||
currentVendoredFileHandle,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
Severity,
|
||||
type Workspace,
|
||||
CompletionKind,
|
||||
type FileHandle,
|
||||
DocumentHighlight,
|
||||
DocumentHighlightKind,
|
||||
} from "ty_wasm";
|
||||
|
|
@ -44,6 +45,9 @@ type Props = {
|
|||
onChange(content: string): void;
|
||||
onMount(editor: IStandaloneCodeEditor, monaco: Monaco): void;
|
||||
onOpenFile(file: FileId): void;
|
||||
onVendoredFileChange: (vendoredFileHandle: FileHandle) => void;
|
||||
onBackToUserFile: () => void;
|
||||
isViewingVendoredFile: boolean;
|
||||
};
|
||||
|
||||
export default function Editor({
|
||||
|
|
@ -57,6 +61,9 @@ export default function Editor({
|
|||
onChange,
|
||||
onMount,
|
||||
onOpenFile,
|
||||
onVendoredFileChange,
|
||||
onBackToUserFile,
|
||||
isViewingVendoredFile = false,
|
||||
}: Props) {
|
||||
const serverRef = useRef<PlaygroundServer | null>(null);
|
||||
|
||||
|
|
@ -65,6 +72,8 @@ export default function Editor({
|
|||
files,
|
||||
workspace,
|
||||
onOpenFile,
|
||||
onVendoredFileChange,
|
||||
onBackToUserFile,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -81,9 +90,12 @@ export default function Editor({
|
|||
|
||||
const handleChange = useCallback(
|
||||
(value: string | undefined) => {
|
||||
onChange(value ?? "");
|
||||
// Don't update file content when viewing vendored files
|
||||
if (!isViewingVendoredFile) {
|
||||
onChange(value ?? "");
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
[onChange, isViewingVendoredFile],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -100,10 +112,12 @@ export default function Editor({
|
|||
(editor, instance) => {
|
||||
serverRef.current?.dispose();
|
||||
|
||||
const server = new PlaygroundServer(instance, {
|
||||
const server = new PlaygroundServer(instance, editor, {
|
||||
workspace,
|
||||
files,
|
||||
onOpenFile,
|
||||
onVendoredFileChange,
|
||||
onBackToUserFile,
|
||||
});
|
||||
|
||||
server.updateDiagnostics(diagnostics);
|
||||
|
|
@ -112,7 +126,15 @@ export default function Editor({
|
|||
onMount(editor, instance);
|
||||
},
|
||||
|
||||
[files, onOpenFile, workspace, onMount, diagnostics],
|
||||
[
|
||||
files,
|
||||
onOpenFile,
|
||||
workspace,
|
||||
onMount,
|
||||
diagnostics,
|
||||
onVendoredFileChange,
|
||||
onBackToUserFile,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -121,7 +143,7 @@ export default function Editor({
|
|||
onMount={handleMount}
|
||||
options={{
|
||||
fixedOverflowWidgets: true,
|
||||
readOnly: false,
|
||||
readOnly: isViewingVendoredFile, // Make editor read-only for vendored files
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
roundedSelection: false,
|
||||
|
|
@ -143,6 +165,8 @@ interface PlaygroundServerProps {
|
|||
workspace: Workspace;
|
||||
files: ReadonlyFiles;
|
||||
onOpenFile: (file: FileId) => void;
|
||||
onVendoredFileChange: (vendoredFileHandle: FileHandle) => void;
|
||||
onBackToUserFile: () => void;
|
||||
}
|
||||
|
||||
class PlaygroundServer
|
||||
|
|
@ -174,9 +198,18 @@ class PlaygroundServer
|
|||
private rangeSemanticTokensDisposable: IDisposable;
|
||||
private signatureHelpDisposable: IDisposable;
|
||||
private documentHighlightDisposable: IDisposable;
|
||||
// Cache for vendored file handles
|
||||
private vendoredFileHandles = new Map<string, FileHandle>();
|
||||
|
||||
private getVendoredPath(uri: Uri): string {
|
||||
// Monaco parses "vendored://stdlib/typing.pyi" as authority="stdlib", path="/typing.pyi"
|
||||
// We need to reconstruct the full path
|
||||
return uri.authority ? `${uri.authority}${uri.path}` : uri.path;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private monaco: Monaco,
|
||||
private editor: IStandaloneCodeEditor,
|
||||
private props: PlaygroundServerProps,
|
||||
) {
|
||||
this.typeDefinitionProviderDisposable =
|
||||
|
|
@ -213,6 +246,9 @@ class PlaygroundServer
|
|||
monaco.languages.registerSignatureHelpProvider("python", this);
|
||||
this.documentHighlightDisposable =
|
||||
monaco.languages.registerDocumentHighlightProvider("python", this);
|
||||
|
||||
// Register Esc key command
|
||||
editor.addCommand(monaco.KeyCode.Escape, this.props.onBackToUserFile);
|
||||
}
|
||||
|
||||
triggerCharacters: string[] = ["."];
|
||||
|
|
@ -229,19 +265,12 @@ class PlaygroundServer
|
|||
provideDocumentSemanticTokens(
|
||||
model: editor.ITextModel,
|
||||
): languages.SemanticTokens | null {
|
||||
const selectedFile = this.props.files.selected;
|
||||
|
||||
if (selectedFile == null) {
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokens = this.props.workspace.semanticTokens(selectedHandle);
|
||||
const tokens = this.props.workspace.semanticTokens(fileHandle);
|
||||
return generateMonacoTokens(tokens, model);
|
||||
}
|
||||
|
||||
|
|
@ -251,25 +280,16 @@ class PlaygroundServer
|
|||
model: editor.ITextModel,
|
||||
range: Range,
|
||||
): languages.SemanticTokens | null {
|
||||
const selectedFile = this.props.files.selected;
|
||||
|
||||
if (selectedFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tyRange = monacoRangeToTyRange(range);
|
||||
|
||||
const tokens = this.props.workspace.semanticTokensInRange(
|
||||
selectedHandle,
|
||||
fileHandle,
|
||||
tyRange,
|
||||
);
|
||||
|
||||
return generateMonacoTokens(tokens, model);
|
||||
}
|
||||
|
||||
|
|
@ -277,20 +297,13 @@ class PlaygroundServer
|
|||
model: editor.ITextModel,
|
||||
position: Position,
|
||||
): languages.ProviderResult<languages.CompletionList> {
|
||||
const selectedFile = this.props.files.selected;
|
||||
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
return;
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const completions = this.props.workspace.completions(
|
||||
selectedHandle,
|
||||
fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
|
|
@ -324,20 +337,13 @@ class PlaygroundServer
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_context: languages.SignatureHelpContext,
|
||||
): languages.ProviderResult<languages.SignatureHelpResult> {
|
||||
const selectedFile = this.props.files.selected;
|
||||
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
return;
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const signatureHelp = this.props.workspace.signatureHelp(
|
||||
selectedHandle,
|
||||
fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
|
|
@ -345,30 +351,7 @@ class PlaygroundServer
|
|||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
dispose() {},
|
||||
value: {
|
||||
signatures: signatureHelp.signatures.map((sig) => ({
|
||||
label: sig.label,
|
||||
documentation: sig.documentation
|
||||
? { value: sig.documentation }
|
||||
: undefined,
|
||||
parameters: sig.parameters.map((param) => ({
|
||||
label: param.label,
|
||||
documentation: param.documentation
|
||||
? { value: param.documentation }
|
||||
: undefined,
|
||||
})),
|
||||
activeParameter: sig.active_parameter,
|
||||
})),
|
||||
activeSignature: signatureHelp.active_signature ?? 0,
|
||||
activeParameter:
|
||||
signatureHelp.active_signature != null
|
||||
? (signatureHelp.signatures[signatureHelp.active_signature]
|
||||
?.active_parameter ?? 0)
|
||||
: 0,
|
||||
},
|
||||
};
|
||||
return this.formatSignatureHelp(signatureHelp);
|
||||
}
|
||||
|
||||
provideDocumentHighlights(
|
||||
|
|
@ -402,26 +385,18 @@ class PlaygroundServer
|
|||
}
|
||||
|
||||
provideInlayHints(
|
||||
_model: editor.ITextModel,
|
||||
model: editor.ITextModel,
|
||||
range: Range,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_token: CancellationToken,
|
||||
): languages.ProviderResult<languages.InlayHintList> {
|
||||
const workspace = this.props.workspace;
|
||||
const selectedFile = this.props.files.selected;
|
||||
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inlayHints = workspace.inlayHints(
|
||||
selectedHandle,
|
||||
const inlayHints = this.props.workspace.inlayHints(
|
||||
fileHandle,
|
||||
monacoRangeToTyRange(range),
|
||||
);
|
||||
|
||||
|
|
@ -454,6 +429,66 @@ class PlaygroundServer
|
|||
this.props = props;
|
||||
}
|
||||
|
||||
private getOrCreateVendoredFileHandle(vendoredPath: string): FileHandle {
|
||||
const cachedHandle = this.vendoredFileHandles.get(vendoredPath);
|
||||
// Check if we already have a handle for this vendored file
|
||||
if (cachedHandle != null) {
|
||||
return cachedHandle;
|
||||
}
|
||||
|
||||
// Use the new WASM method to get a proper file handle for the vendored file
|
||||
const handle = this.props.workspace.getVendoredFile(vendoredPath);
|
||||
this.vendoredFileHandles.set(vendoredPath, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
private getFileHandleForModel(model: editor.ITextModel) {
|
||||
// Handle vendored files
|
||||
if (model.uri.scheme === "vendored") {
|
||||
const vendoredPath = this.getVendoredPath(model.uri);
|
||||
|
||||
// If not cached, try to create it
|
||||
return this.getOrCreateVendoredFileHandle(vendoredPath);
|
||||
}
|
||||
|
||||
// Handle regular user files
|
||||
const selectedFile = this.props.files.selected;
|
||||
if (selectedFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.props.files.handles[selectedFile];
|
||||
}
|
||||
|
||||
private formatSignatureHelp(
|
||||
signatureHelp: any,
|
||||
): languages.SignatureHelpResult {
|
||||
return {
|
||||
dispose() {},
|
||||
value: {
|
||||
signatures: signatureHelp.signatures.map((sig: any) => ({
|
||||
label: sig.label,
|
||||
documentation: sig.documentation
|
||||
? { value: sig.documentation }
|
||||
: undefined,
|
||||
parameters: sig.parameters.map((param: any) => ({
|
||||
label: param.label,
|
||||
documentation: param.documentation
|
||||
? { value: param.documentation }
|
||||
: undefined,
|
||||
})),
|
||||
activeParameter: sig.active_parameter,
|
||||
})),
|
||||
activeSignature: signatureHelp.active_signature ?? 0,
|
||||
activeParameter:
|
||||
signatureHelp.active_signature != null
|
||||
? (signatureHelp.signatures[signatureHelp.active_signature]
|
||||
?.active_parameter ?? 0)
|
||||
: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
updateDiagnostics(diagnostics: Array<Diagnostic>) {
|
||||
if (this.props.files.selected == null) {
|
||||
return;
|
||||
|
|
@ -513,26 +548,18 @@ class PlaygroundServer
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
context?: languages.HoverContext<languages.Hover> | undefined,
|
||||
): languages.ProviderResult<languages.Hover> {
|
||||
const workspace = this.props.workspace;
|
||||
|
||||
const selectedFile = this.props.files.selected;
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hover = workspace.hover(
|
||||
selectedHandle,
|
||||
const hover = this.props.workspace.hover(
|
||||
fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
if (hover == null) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -547,21 +574,13 @@ class PlaygroundServer
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_: CancellationToken,
|
||||
): languages.ProviderResult<languages.Definition | languages.LocationLink[]> {
|
||||
const workspace = this.props.workspace;
|
||||
|
||||
const selectedFile = this.props.files.selected;
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = workspace.gotoTypeDefinition(
|
||||
selectedHandle,
|
||||
const links = this.props.workspace.gotoTypeDefinition(
|
||||
fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
|
|
@ -574,21 +593,13 @@ class PlaygroundServer
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_: CancellationToken,
|
||||
): languages.ProviderResult<languages.Definition | languages.LocationLink[]> {
|
||||
const workspace = this.props.workspace;
|
||||
|
||||
const selectedFile = this.props.files.selected;
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = workspace.gotoDeclaration(
|
||||
selectedHandle,
|
||||
const links = this.props.workspace.gotoDeclaration(
|
||||
fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
|
|
@ -601,21 +612,13 @@ class PlaygroundServer
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_: CancellationToken,
|
||||
): languages.ProviderResult<languages.Definition | languages.LocationLink[]> {
|
||||
const workspace = this.props.workspace;
|
||||
|
||||
const selectedFile = this.props.files.selected;
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = workspace.gotoDefinition(
|
||||
selectedHandle,
|
||||
const links = this.props.workspace.gotoDefinition(
|
||||
fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
|
|
@ -630,21 +633,13 @@ class PlaygroundServer
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_: CancellationToken,
|
||||
): languages.ProviderResult<languages.Location[]> {
|
||||
const workspace = this.props.workspace;
|
||||
|
||||
const selectedFile = this.props.files.selected;
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
const fileHandle = this.getFileHandleForModel(model);
|
||||
if (fileHandle == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedHandle = this.props.files.handles[selectedFile];
|
||||
|
||||
if (selectedHandle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = workspace.gotoReferences(
|
||||
selectedHandle,
|
||||
const links = this.props.workspace.gotoReferences(
|
||||
fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
|
|
@ -658,34 +653,73 @@ class PlaygroundServer
|
|||
): boolean {
|
||||
const files = this.props.files;
|
||||
|
||||
const fileId = files.index.find((file) => {
|
||||
return Uri.file(file.name).toString() === resource.toString();
|
||||
})?.id;
|
||||
// Check if this is a vendored file
|
||||
if (resource.scheme === "vendored") {
|
||||
const vendoredPath = this.getVendoredPath(resource);
|
||||
// Get a file handle for this vendored file
|
||||
const fileHandle = this.getOrCreateVendoredFileHandle(vendoredPath);
|
||||
|
||||
if (fileId == null) {
|
||||
return false;
|
||||
}
|
||||
// Create or get the model for the vendored file
|
||||
let model = this.monaco.editor.getModel(resource);
|
||||
|
||||
const handle = files.handles[fileId];
|
||||
if (model == null) {
|
||||
// Read the vendored file content using the file handle
|
||||
const content = this.props.workspace.sourceText(fileHandle);
|
||||
// Ensure vendored files get proper Python language features
|
||||
model = this.monaco.editor.createModel(content, "python", resource);
|
||||
}
|
||||
|
||||
let model = this.monaco.editor.getModel(resource);
|
||||
if (model == null) {
|
||||
const language =
|
||||
handle != null && isPythonFile(handle) ? "python" : undefined;
|
||||
model = this.monaco.editor.createModel(
|
||||
files.contents[fileId],
|
||||
language,
|
||||
resource,
|
||||
);
|
||||
}
|
||||
|
||||
// it's a bit hacky to create the model manually
|
||||
// but only using `onOpenFile` isn't enough
|
||||
// because the model doesn't get updated until the next render.
|
||||
if (files.selected !== fileId) {
|
||||
// Set the model and reveal the position
|
||||
source.setModel(model);
|
||||
|
||||
this.props.onOpenFile(fileId);
|
||||
if (selectionOrPosition != null) {
|
||||
if (Position.isIPosition(selectionOrPosition)) {
|
||||
source.setPosition(selectionOrPosition);
|
||||
source.revealPositionInCenterIfOutsideViewport(selectionOrPosition);
|
||||
} else {
|
||||
source.setSelection(selectionOrPosition);
|
||||
source.revealRangeNearTopIfOutsideViewport(selectionOrPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// Track that we're now viewing a vendored file
|
||||
this.props.onVendoredFileChange(fileHandle);
|
||||
} else {
|
||||
// Handle regular files
|
||||
const fileId = files.index.find((file) => {
|
||||
return Uri.file(file.name).toString() === resource.toString();
|
||||
})?.id;
|
||||
|
||||
if (fileId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const handle = files.handles[fileId];
|
||||
if (handle == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let model = this.monaco.editor.getModel(resource);
|
||||
if (model == null) {
|
||||
const language = isPythonFile(handle) ? "python" : undefined;
|
||||
model = this.monaco.editor.createModel(
|
||||
files.contents[fileId],
|
||||
language,
|
||||
resource,
|
||||
);
|
||||
} else {
|
||||
// Update model content to match current file state
|
||||
model.setValue(files.contents[fileId]);
|
||||
}
|
||||
|
||||
// it's a bit hacky to create the model manually
|
||||
// but only using `onOpenFile` isn't enough
|
||||
// because the model doesn't get updated until the next render.
|
||||
if (files.selected !== fileId) {
|
||||
source.setModel(model);
|
||||
|
||||
this.props.onOpenFile(fileId);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionOrPosition != null) {
|
||||
|
|
@ -797,26 +831,28 @@ function generateMonacoTokens(
|
|||
}
|
||||
|
||||
function mapNavigationTargets(links: any[]): languages.LocationLink[] {
|
||||
return links
|
||||
.map((link) => {
|
||||
const targetSelection =
|
||||
link.selection_range == null
|
||||
? undefined
|
||||
: tyRangeToMonacoRange(link.selection_range);
|
||||
const result = links.map((link) => {
|
||||
const targetSelection =
|
||||
link.selection_range == null
|
||||
? undefined
|
||||
: tyRangeToMonacoRange(link.selection_range);
|
||||
|
||||
const originSelection =
|
||||
link.origin_selection_range == null
|
||||
? undefined
|
||||
: tyRangeToMonacoRange(link.origin_selection_range);
|
||||
const originSelection =
|
||||
link.origin_selection_range == null
|
||||
? undefined
|
||||
: tyRangeToMonacoRange(link.origin_selection_range);
|
||||
|
||||
return {
|
||||
uri: Uri.parse(link.path),
|
||||
range: tyRangeToMonacoRange(link.full_range),
|
||||
targetSelectionRange: targetSelection,
|
||||
originSelectionRange: originSelection,
|
||||
} as languages.LocationLink;
|
||||
})
|
||||
.filter((link) => link.uri.scheme !== "vendored");
|
||||
const locationLink = {
|
||||
uri: Uri.parse(link.path),
|
||||
range: tyRangeToMonacoRange(link.full_range),
|
||||
targetSelectionRange: targetSelection,
|
||||
originSelectionRange: originSelection,
|
||||
} as languages.LocationLink;
|
||||
|
||||
return locationLink;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function mapCompletionKind(kind: CompletionKind): CompletionItemKind {
|
||||
|
|
|
|||
|
|
@ -199,6 +199,6 @@ function FileEntry({ name, onClicked, onRenamed, selected }: FileEntryProps) {
|
|||
}
|
||||
|
||||
export function isPythonFile(handle: FileHandle): boolean {
|
||||
const extension = handle?.path().toLowerCase().split(".").pop() ?? "";
|
||||
const extension = handle.path().toLowerCase().split(".").pop() ?? "";
|
||||
return ["py", "pyi", "pyw"].includes(extension);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,9 @@ function Content({
|
|||
case "error":
|
||||
return (
|
||||
<div className="flex-grow">
|
||||
<code className="whitespace-pre-wrap">{result.error}</code>
|
||||
<code className="whitespace-pre-wrap text-gray-900 dark:text-gray-100">
|
||||
{result.error}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import type { FileId } from "../Playground";
|
||||
import type { FileHandle } from "ty_wasm";
|
||||
|
||||
interface Props {
|
||||
currentVendoredFile: FileHandle;
|
||||
selectedFile: { id: FileId; name: string };
|
||||
onBackToUserFile: () => void;
|
||||
}
|
||||
|
||||
export default function VendoredFileBanner({
|
||||
currentVendoredFile,
|
||||
selectedFile,
|
||||
onBackToUserFile,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900 px-3 py-2 border-b border-blue-200 dark:border-blue-700 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="font-medium text-blue-800 dark:text-blue-200">
|
||||
Viewing standard library file:
|
||||
</span>{" "}
|
||||
<code className="font-mono text-blue-700 dark:text-blue-300">
|
||||
{currentVendoredFile.path()}
|
||||
</code>
|
||||
<span className="text-blue-600 dark:text-blue-400 ml-2 text-xs">
|
||||
(read-only)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onBackToUserFile}
|
||||
className="px-3 py-1 text-xs bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200 rounded border border-blue-300 dark:border-blue-600 hover:bg-blue-200 dark:hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Back to {selectedFile.name}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -128,6 +128,14 @@ export default function Playground() {
|
|||
dispatchFiles({ type: "selectFile", id: file });
|
||||
}, []);
|
||||
|
||||
const handleVendoredFileSelected = useCallback((handle: FileHandle) => {
|
||||
dispatchFiles({ type: "selectVendoredFile", handle });
|
||||
}, []);
|
||||
|
||||
const handleVendoredFileCleared = useCallback(() => {
|
||||
dispatchFiles({ type: "clearVendoredFile" });
|
||||
}, []);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
if (workspace == null) {
|
||||
return;
|
||||
|
|
@ -174,6 +182,8 @@ export default function Playground() {
|
|||
onRemoveFile={handleFileRemoved}
|
||||
onSelectFile={handleFileSelected}
|
||||
onChangeFile={handleFileChanged}
|
||||
onSelectVendoredFile={handleVendoredFileSelected}
|
||||
onClearVendoredFile={handleVendoredFileCleared}
|
||||
/>
|
||||
</Suspense>
|
||||
{error ? (
|
||||
|
|
@ -289,6 +299,11 @@ interface FilesState {
|
|||
playgroundRevision: number;
|
||||
|
||||
nextId: FileId;
|
||||
|
||||
/**
|
||||
* The currently viewed vendored/builtin file, if any.
|
||||
*/
|
||||
currentVendoredFile: FileHandle | null;
|
||||
}
|
||||
|
||||
export type FileAction =
|
||||
|
|
@ -311,7 +326,12 @@ export type FileAction =
|
|||
}
|
||||
| { type: "selectFile"; id: FileId }
|
||||
| { type: "selectFileByName"; name: string }
|
||||
| { type: "reset" };
|
||||
| { type: "reset" }
|
||||
| {
|
||||
type: "selectVendoredFile";
|
||||
handle: FileHandle;
|
||||
}
|
||||
| { type: "clearVendoredFile" };
|
||||
|
||||
const INIT_FILES_STATE: ReadonlyFiles = {
|
||||
index: [],
|
||||
|
|
@ -321,6 +341,7 @@ const INIT_FILES_STATE: ReadonlyFiles = {
|
|||
revision: 0,
|
||||
selected: null,
|
||||
playgroundRevision: 0,
|
||||
currentVendoredFile: null,
|
||||
};
|
||||
|
||||
function filesReducer(
|
||||
|
|
@ -339,6 +360,7 @@ function filesReducer(
|
|||
contents: { ...state.contents, [id]: content },
|
||||
nextId: state.nextId + 1,
|
||||
revision: state.revision + 1,
|
||||
currentVendoredFile: null, // Clear vendored file when adding new file
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -375,6 +397,7 @@ function filesReducer(
|
|||
contents,
|
||||
handles,
|
||||
revision: state.revision + 1,
|
||||
currentVendoredFile: null, // Clear vendored file when removing file
|
||||
};
|
||||
}
|
||||
case "rename": {
|
||||
|
|
@ -397,6 +420,7 @@ function filesReducer(
|
|||
return {
|
||||
...state,
|
||||
selected: id,
|
||||
currentVendoredFile: null, // Clear vendored file when selecting regular file
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -409,6 +433,7 @@ function filesReducer(
|
|||
return {
|
||||
...state,
|
||||
selected,
|
||||
currentVendoredFile: null, // Clear vendored file when selecting regular file
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -419,6 +444,22 @@ function filesReducer(
|
|||
revision: state.revision + 1,
|
||||
};
|
||||
}
|
||||
|
||||
case "selectVendoredFile": {
|
||||
const { handle } = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentVendoredFile: handle,
|
||||
};
|
||||
}
|
||||
|
||||
case "clearVendoredFile": {
|
||||
return {
|
||||
...state,
|
||||
currentVendoredFile: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue