Send telemetry (#1702)

* restart on auth

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix deps

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* hash the iuser

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add comment;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* zip up the contents

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix logic

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more code coverage

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* u[dates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* u[dates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* up[dates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more coverage

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more coverage

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup dead code

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of accept / reject

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* accept/reject

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2024-03-12 23:57:43 -07:00
committed by GitHub
parent dfc51e6c30
commit 2d979b56f5
17 changed files with 1726 additions and 155 deletions

View File

@ -62,8 +62,16 @@ jobs:
shell: bash shell: bash
run: |- run: |-
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
cargo nextest run --workspace --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log cargo llvm-cov nextest --all --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
env: env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000 RUST_MIN_STACK: 10485760000
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
with:
token: ${{secrets.CODECOV_TOKEN}}
fail_ci_if_error: true
flags: wasm-lib
verbose: true
files: lcov.info

View File

@ -40,9 +40,11 @@ export function App() {
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { onProjectOpen } = useLspContext() const { onProjectOpen } = useLspContext()
const projectName = project?.name || null
const projectPath = project?.path || null
useEffect(() => { useEffect(() => {
onProjectOpen(project || null, file || null) onProjectOpen({ name: projectName, path: projectPath }, file || null)
}, []) }, [projectName, projectPath])
useHotKeyListener() useHotKeyListener()
const { const {

View File

@ -14,7 +14,6 @@ import { LanguageSupport } from '@codemirror/language'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { FileEntry } from '@tauri-apps/api/fs' import { FileEntry } from '@tauri-apps/api/fs'
import { ProjectWithEntryPointMetadata } from 'lib/types'
const DEFAULT_FILE_NAME: string = 'main.kcl' const DEFAULT_FILE_NAME: string = 'main.kcl'
@ -40,7 +39,7 @@ type LspContext = {
redirect: boolean redirect: boolean
) => void ) => void
onProjectOpen: ( onProjectOpen: (
project: ProjectWithEntryPointMetadata | null, project: { name: string | null; path: string | null } | null,
file: FileEntry | null file: FileEntry | null
) => void ) => void
onFileOpen: (filePath: string | null, projectPath: string | null) => void onFileOpen: (filePath: string | null, projectPath: string | null) => void
@ -69,6 +68,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
})) }))
const { auth } = useSettingsAuthContext() const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const navigate = useNavigate() const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client. // So this is a bit weird, we need to initialize the lsp server and client.
@ -80,7 +80,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const client = new Client(fromServer, intoServer) const client = new Client(fromServer, intoServer)
if (!TEST) { if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => { Server.initialize(intoServer, fromServer).then((lspServer) => {
const token = auth?.context?.token
lspServer.start('kcl', token) lspServer.start('kcl', token)
setIsKclLspServerReady(true) setIsKclLspServerReady(true)
}) })
@ -88,7 +87,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const lspClient = new LanguageServerClient({ client, name: 'kcl' }) const lspClient = new LanguageServerClient({ client, name: 'kcl' })
return { lspClient } return { lspClient }
}, [setIsKclLspServerReady]) }, [setIsKclLspServerReady, token])
// Here we initialize the plugin which will start the client. // Here we initialize the plugin which will start the client.
// Now that we have multi-file support the name of the file is a dep of // Now that we have multi-file support the name of the file is a dep of
@ -116,7 +115,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const client = new Client(fromServer, intoServer) const client = new Client(fromServer, intoServer)
if (!TEST) { if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => { Server.initialize(intoServer, fromServer).then((lspServer) => {
const token = auth?.context?.token
lspServer.start('copilot', token) lspServer.start('copilot', token)
setIsCopilotLspServerReady(true) setIsCopilotLspServerReady(true)
}) })
@ -124,7 +122,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const lspClient = new LanguageServerClient({ client, name: 'copilot' }) const lspClient = new LanguageServerClient({ client, name: 'copilot' })
return { lspClient } return { lspClient }
}, [setIsCopilotLspServerReady]) }, [setIsCopilotLspServerReady, token])
// Here we initialize the plugin which will start the client. // Here we initialize the plugin which will start the client.
// When we have multi-file support the name of the file will be a dep of // When we have multi-file support the name of the file will be a dep of
@ -172,7 +170,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
} }
const onProjectOpen = ( const onProjectOpen = (
project: ProjectWithEntryPointMetadata | null, project: { name: string | null; path: string | null } | null,
file: FileEntry | null file: FileEntry | null
) => { ) => {
const projectName = project?.name || 'ProjectRoot' const projectName = project?.name || 'ProjectRoot'

View File

@ -2,67 +2,10 @@ import type * as LSP from 'vscode-languageserver-protocol'
import Client from './client' import Client from './client'
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens' import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin' import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
export interface CopilotGetCompletionsParams { import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
doc: { import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
source: string import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
tabSize: number
indentSize: number
insertSpaces: boolean
path: string
uri: string
relativePath: string
languageId: string
position: {
line: number
character: number
}
}
}
interface CopilotGetCompletionsResult {
completions: {
text: string
position: {
line: number
character: number
}
uuid: string
range: {
start: {
line: number
character: number
}
end: {
line: number
character: number
}
}
displayText: string
point: {
line: number
character: number
}
region: {
start: {
line: number
character: number
}
end: {
line: number
character: number
}
}
}[]
}
interface CopilotAcceptCompletionParams {
uuid: string
}
interface CopilotRejectCompletionParams {
uuids: string[]
}
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/ // https://microsoft.github.io/language-server-protocol/specifications/specification-current/
@ -78,7 +21,7 @@ interface LSPRequestMap {
LSP.SemanticTokensParams, LSP.SemanticTokensParams,
LSP.SemanticTokens LSP.SemanticTokens
] ]
getCompletions: [CopilotGetCompletionsParams, CopilotGetCompletionsResult] getCompletions: [CopilotLspCompletionParams, CopilotCompletionResponse]
notifyAccepted: [CopilotAcceptCompletionParams, any] notifyAccepted: [CopilotAcceptCompletionParams, any]
notifyRejected: [CopilotRejectCompletionParams, any] notifyRejected: [CopilotRejectCompletionParams, any]
} }
@ -271,7 +214,7 @@ export class LanguageServerClient {
return this.client.notify(method, params) return this.client.notify(method, params)
} }
async getCompletion(params: CopilotGetCompletionsParams) { async getCompletion(params: CopilotLspCompletionParams) {
const response = await this.request('getCompletions', params) const response = await this.request('getCompletions', params)
// //
this.queuedUids = [...response.completions.map((c) => c.uuid)] this.queuedUids = [...response.completions.map((c) => c.uuid)]

View File

@ -37,6 +37,7 @@ import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSingleton'
import { useLspContext } from 'components/LspProvider'
// This route only opens in the Tauri desktop context for now, // This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types. // as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -51,6 +52,7 @@ const Home = () => {
send: sendToSettings, send: sendToSettings,
}, },
} = useSettingsAuthContext() } = useSettingsAuthContext()
const { onProjectOpen } = useLspContext()
// Set the default directory if it's been updated // Set the default directory if it's been updated
// during the loading of the home page. This is wrapped // during the loading of the home page. This is wrapped
@ -84,12 +86,16 @@ const Home = () => {
event: EventFrom<typeof homeMachine> event: EventFrom<typeof homeMachine>
) => { ) => {
if (event.data && 'name' in event.data) { if (event.data && 'name' in event.data) {
commandBarSend({ type: 'Close' }) let projectPath = context.defaultDirectory + sep + event.data.name
navigate( onProjectOpen(
`${paths.FILE}/${encodeURIComponent( {
context.defaultDirectory + sep + event.data.name name: event.data.name,
)}` path: projectPath,
},
null
) )
commandBarSend({ type: 'Close' })
navigate(`${paths.FILE}/${encodeURIComponent(projectPath)}`)
} }
}, },
toastSuccess: (_, event) => toast.success((event.data || '') + ''), toastSuccess: (_, event) => toast.success((event.data || '') + ''),

View File

@ -522,9 +522,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.34" version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
@ -1914,6 +1914,7 @@ dependencies = [
"async-recursion", "async-recursion",
"async-trait", "async-trait",
"bson", "bson",
"chrono",
"clap", "clap",
"criterion", "criterion",
"dashmap", "dashmap",
@ -1929,6 +1930,7 @@ dependencies = [
"kittycad-execution-plan-macros", "kittycad-execution-plan-macros",
"kittycad-execution-plan-traits", "kittycad-execution-plan-traits",
"lazy_static", "lazy_static",
"mime_guess",
"parse-display 0.9.0", "parse-display 0.9.0",
"pretty_assertions", "pretty_assertions",
"reqwest", "reqwest",
@ -1936,6 +1938,7 @@ dependencies = [
"schemars", "schemars",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
@ -1946,6 +1949,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"winnow", "winnow",
"zip",
] ]
[[package]] [[package]]
@ -1962,9 +1966,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.2.59" version = "0.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4080db4364c103601db486e4a8aa889ea56c011991e4c454373d8050a165d3da" checksum = "f8aa5906d0730bd90f6b3331fe57c04951d00743169a29ee96408767b4060605"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1978,6 +1982,7 @@ dependencies = [
"http 0.2.9", "http 0.2.9",
"itertools 0.10.5", "itertools 0.10.5",
"log", "log",
"mime_guess",
"parse-display 0.8.2", "parse-display 0.8.2",
"phonenumber", "phonenumber",
"rand 0.8.5", "rand 0.8.5",
@ -2000,7 +2005,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan" name = "kittycad-execution-plan"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [ dependencies = [
"bytes", "bytes",
"insta", "insta",
@ -2020,7 +2025,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan-macros" name = "kittycad-execution-plan-macros"
version = "0.1.8" version = "0.1.8"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2030,7 +2035,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan-traits" name = "kittycad-execution-plan-traits"
version = "0.1.12" version = "0.1.12"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@ -2040,7 +2045,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds" name = "kittycad-modeling-cmds"
version = "0.1.28" version = "0.1.28"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -2068,7 +2073,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds-macros" name = "kittycad-modeling-cmds-macros"
version = "0.1.2" version = "0.1.2"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2077,8 +2082,8 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-session" name = "kittycad-modeling-session"
version = "0.1.0" version = "0.1.1"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [ dependencies = [
"futures", "futures",
"kittycad", "kittycad",
@ -5505,6 +5510,17 @@ dependencies = [
"syn 2.0.52", "syn 2.0.52",
] ]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"byteorder",
"crc32fast",
"crossbeam-utils",
]
[[package]] [[package]]
name = "zune-inflate" name = "zune-inflate"
version = "0.2.54" version = "0.2.54"

View File

@ -59,7 +59,7 @@ members = [
] ]
[workspace.dependencies] [workspace.dependencies]
kittycad = { version = "0.2.59", default-features = false, features = ["js", "requests"] } kittycad = { version = "0.2.60", default-features = false, features = ["js", "requests"] }
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }

View File

@ -14,6 +14,7 @@ keywords = ["kcl", "KittyCAD", "CAD"]
anyhow = { version = "1.0.80", features = ["backtrace"] } anyhow = { version = "1.0.80", features = ["backtrace"] }
async-recursion = "1.0.5" async-recursion = "1.0.5"
async-trait = "0.1.77" async-trait = "0.1.77"
chrono = "0.4.35"
clap = { version = "4.5.2", features = ["cargo", "derive", "env", "unicode"], optional = true } clap = { version = "4.5.2", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3" dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] } databake = { version = "0.1.7", features = ["derive"] }
@ -25,16 +26,19 @@ kittycad = { workspace = true }
kittycad-execution-plan-macros = { workspace = true } kittycad-execution-plan-macros = { workspace = true }
kittycad-execution-plan-traits = { workspace = true } kittycad-execution-plan-traits = { workspace = true }
lazy_static = "1.4.0" lazy_static = "1.4.0"
mime_guess = "2.0.4"
parse-display = "0.9.0" parse-display = "0.9.0"
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] } reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1" ropey = "1.6.1"
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] } schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114" serde_json = "1.0.114"
sha2 = "0.10.8"
thiserror = "1.0.57" thiserror = "1.0.57"
ts-rs = { version = "7.1.1", features = ["uuid-impl"] } ts-rs = { version = "7.1.1", features = ["uuid-impl"] }
uuid = { version = "1.7.0", features = ["v4", "js", "serde"] } uuid = { version = "1.7.0", features = ["v4", "js", "serde"] }
winnow = "0.5.40" winnow = "0.5.40"
zip = { version = "0.6.6", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" } js-sys = { version = "0.3.69" }

View File

@ -75,11 +75,33 @@ pub trait Backend {
} }
async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
// If we are adding a folder that we were previously on, we should not clear the
// state.
let should_clear = if !params.event.added.is_empty() {
let mut should_clear = false;
for folder in params.event.added.iter() {
if !self
.workspace_folders()
.iter()
.any(|f| f.uri == folder.uri && f.name == folder.name)
{
should_clear = true;
break;
}
}
should_clear
} else {
!(params.event.removed.is_empty() && params.event.added.is_empty())
};
self.add_workspace_folders(params.event.added.clone()); self.add_workspace_folders(params.event.added.clone());
self.remove_workspace_folders(params.event.removed); self.remove_workspace_folders(params.event.removed);
// Remove the code from the current code map. // Remove the code from the current code map.
// We do this since it means the user is changing projects so let's refresh the state. // We do this since it means the user is changing projects so let's refresh the state.
if !self.current_code_map().is_empty() && should_clear {
self.clear_code_state(); self.clear_code_state();
}
for added in params.event.added { for added in params.event.added {
// Try to read all the files in the project. // Try to read all the files in the project.
let project_dir = added.uri.to_string().replace("file://", ""); let project_dir = added.uri.to_string().replace("file://", "");
@ -181,14 +203,5 @@ pub trait Backend {
self.client() self.client()
.log_message(MessageType::INFO, format!("document closed: {:?}", params)) .log_message(MessageType::INFO, format!("document closed: {:?}", params))
.await; .await;
self.client()
.log_message(MessageType::INFO, format!("uri: {:?}", params.text_document.uri))
.await;
// Get the workspace folders.
// The key of the workspace folder is the project name.
let workspace_folders = self.workspace_folders();
self.client()
.log_message(MessageType::INFO, format!("workspace: {:?}", workspace_folders))
.await;
} }
} }

View File

@ -29,6 +29,8 @@ use crate::lsp::{
copilot::types::{CopilotCompletionResponse, CopilotEditorInfo, CopilotLspCompletionParams, DocParams}, copilot::types::{CopilotCompletionResponse, CopilotEditorInfo, CopilotLspCompletionParams, DocParams},
}; };
use self::types::{CopilotAcceptCompletionParams, CopilotCompletionTelemetry, CopilotRejectCompletionParams};
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
pub struct Success { pub struct Success {
success: bool, success: bool,
@ -39,7 +41,7 @@ impl Success {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Backend { pub struct Backend {
/// The client is used to send notifications and requests to the client. /// The client is used to send notifications and requests to the client.
pub client: tower_lsp::Client, pub client: tower_lsp::Client,
@ -54,7 +56,9 @@ pub struct Backend {
/// The editor info is used to store information about the editor. /// The editor info is used to store information about the editor.
pub editor_info: Arc<RwLock<CopilotEditorInfo>>, pub editor_info: Arc<RwLock<CopilotEditorInfo>>,
/// The cache is used to store the results of previous requests. /// The cache is used to store the results of previous requests.
pub cache: cache::CopilotCache, pub cache: Arc<cache::CopilotCache>,
/// Storage so we can send telemetry data back out.
pub telemetry: DashMap<uuid::Uuid, CopilotCompletionTelemetry>,
} }
// Implement the shared backend trait for the language server. // Implement the shared backend trait for the language server.
@ -158,7 +162,7 @@ impl Backend {
let pos = params.doc.position; let pos = params.doc.position;
let uri = params.doc.uri.to_string(); let uri = params.doc.uri.to_string();
let rope = ropey::Rope::from_str(&params.doc.source); let rope = ropey::Rope::from_str(&params.doc.source);
let offset = crate::lsp::util::position_to_offset(pos, &rope).unwrap_or_default(); let offset = crate::lsp::util::position_to_offset(pos.into(), &rope).unwrap_or_default();
Ok(DocParams { Ok(DocParams {
uri: uri.to_string(), uri: uri.to_string(),
@ -166,7 +170,7 @@ impl Backend {
language: params.doc.language_id.to_string(), language: params.doc.language_id.to_string(),
prefix: crate::lsp::util::get_text_before(offset, &rope).unwrap_or_default(), prefix: crate::lsp::util::get_text_before(offset, &rope).unwrap_or_default(),
suffix: crate::lsp::util::get_text_after(offset, &rope).unwrap_or_default(), suffix: crate::lsp::util::get_text_after(offset, &rope).unwrap_or_default(),
line_before: crate::lsp::util::get_line_before(pos, &rope).unwrap_or_default(), line_before: crate::lsp::util::get_line_before(pos.into(), &rope).unwrap_or_default(),
rope, rope,
}) })
} }
@ -185,37 +189,69 @@ impl Backend {
let line_before = doc_params.line_before.to_string(); let line_before = doc_params.line_before.to_string();
// Let's not call it yet since it's not our model. // Let's not call it yet since it's not our model.
/*let completion_list = self // We will need to wrap in spawn_local like we do in kcl/mod.rs for wasm only.
#[cfg(test)]
let completion_list = self
.get_completions(doc_params.language, doc_params.prefix, doc_params.suffix) .get_completions(doc_params.language, doc_params.prefix, doc_params.suffix)
.await .await
.map_err(|err| Error { .map_err(|err| Error {
code: tower_lsp::jsonrpc::ErrorCode::from(69), code: tower_lsp::jsonrpc::ErrorCode::from(69),
data: None, data: None,
message: Cow::from(format!("Failed to get completions: {}", err)), message: Cow::from(format!("Failed to get completions: {}", err)),
})?;*/ })?;
#[cfg(not(test))]
let completion_list = vec![]; let completion_list = vec![];
let response = CopilotCompletionResponse::from_str_vec(completion_list, line_before, doc_params.pos); let response = CopilotCompletionResponse::from_str_vec(completion_list, line_before, doc_params.pos);
// Set the telemetry data for each completion.
for completion in response.completions.iter() {
let telemetry = CopilotCompletionTelemetry {
completion: completion.clone(),
params: params.clone(),
};
self.telemetry.insert(completion.uuid, telemetry);
}
self.cache self.cache
.set_cached_result(&doc_params.uri, &doc_params.pos.line, &response); .set_cached_result(&doc_params.uri, &doc_params.pos.line, &response);
Ok(response) Ok(response)
} }
pub async fn accept_completions(&self, params: Vec<String>) { pub async fn accept_completion(&self, params: CopilotAcceptCompletionParams) {
self.client self.client
.log_message(MessageType::INFO, format!("Accepted completions: {:?}", params)) .log_message(MessageType::INFO, format!("Accepted completions: {:?}", params))
.await; .await;
// TODO: send telemetry data back out that we accepted the completions // Get the original telemetry data.
let Some((_, original)) = self.telemetry.remove(&params.uuid) else {
return;
};
self.client
.log_message(MessageType::INFO, format!("Original telemetry: {:?}", original))
.await;
// TODO: Send the telemetry data to the zoo api.
} }
pub async fn reject_completions(&self, params: Vec<String>) { pub async fn reject_completions(&self, params: CopilotRejectCompletionParams) {
self.client self.client
.log_message(MessageType::INFO, format!("Rejected completions: {:?}", params)) .log_message(MessageType::INFO, format!("Rejected completions: {:?}", params))
.await; .await;
// TODO: send telemetry data back out that we rejected the completions // Get the original telemetry data.
let mut originals: Vec<CopilotCompletionTelemetry> = Default::default();
for uuid in params.uuids {
if let Some((_, original)) = self.telemetry.remove(&uuid) {
originals.push(original);
}
}
self.client
.log_message(MessageType::INFO, format!("Original telemetry: {:?}", originals))
.await;
// TODO: Send the telemetry data to the zoo api.
} }
} }

View File

@ -2,18 +2,58 @@
use ropey::Rope; use ropey::Rope;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::{Position, Range};
#[derive(Debug, Serialize, Deserialize, Clone)] /// Position in a text document expressed as zero-based line and character offset.
/// A position is between two characters like an 'insert' cursor in a editor.
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default, Deserialize, Serialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CopilotCyclingCompletion { #[ts(export)]
pub display_text: String, // partial text pub struct CopilotPosition {
pub text: String, // fulltext /// Line position in a document (zero-based).
pub range: Range, // start char always 0 pub line: u32,
pub position: Position, /// Character offset on a line in a document (zero-based). The meaning of this
/// offset is determined by the negotiated `PositionEncodingKind`.
///
/// If the character value is greater than the line length it defaults back
/// to the line length.
pub character: u32,
} }
#[derive(Debug, Serialize, Deserialize)] impl From<CopilotPosition> for tower_lsp::lsp_types::Position {
fn from(position: CopilotPosition) -> Self {
tower_lsp::lsp_types::Position {
line: position.line,
character: position.character,
}
}
}
/// A range in a text document expressed as (zero-based) start and end positions.
/// A range is comparable to a selection in an editor. Therefore the end position is exclusive.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Deserialize, Serialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotRange {
/// The range's start position.
pub start: CopilotPosition,
/// The range's end position.
pub end: CopilotPosition,
}
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotCyclingCompletion {
pub uuid: uuid::Uuid, // unique id we use for tracking accepted or rejected completions
pub display_text: String, // partial text
pub text: String, // fulltext
pub range: CopilotRange, // start char always 0
pub position: CopilotPosition,
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Choices { pub struct Choices {
pub text: String, pub text: String,
pub index: i16, pub index: i16,
@ -21,14 +61,16 @@ pub struct Choices {
pub logprobs: Option<String>, pub logprobs: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotCompletionResponse { pub struct CopilotCompletionResponse {
pub completions: Vec<CopilotCyclingCompletion>, pub completions: Vec<CopilotCyclingCompletion>,
pub cancellation_reason: Option<String>, pub cancellation_reason: Option<String>,
} }
impl CopilotCompletionResponse { impl CopilotCompletionResponse {
pub fn from_str_vec(str_vec: Vec<String>, line_before: String, pos: Position) -> Self { pub fn from_str_vec(str_vec: Vec<String>, line_before: String, pos: CopilotPosition) -> Self {
let completions = str_vec let completions = str_vec
.iter() .iter()
.map(|x| CopilotCyclingCompletion::new(x.to_string(), line_before.to_string(), pos)) .map(|x| CopilotCyclingCompletion::new(x.to_string(), line_before.to_string(), pos))
@ -41,19 +83,20 @@ impl CopilotCompletionResponse {
} }
impl CopilotCyclingCompletion { impl CopilotCyclingCompletion {
pub fn new(text: String, line_before: String, position: Position) -> Self { pub fn new(text: String, line_before: String, position: CopilotPosition) -> Self {
let display_text = text.clone(); let display_text = text.clone();
let text = format!("{}{}", line_before, text); let text = format!("{}{}", line_before, text);
let end_char = text.find('\n').unwrap_or(text.len()) as u32; let end_char = text.find('\n').unwrap_or(text.len()) as u32;
Self { Self {
uuid: uuid::Uuid::new_v4(),
display_text, // partial text display_text, // partial text
text, // fulltext text, // fulltext
range: Range { range: CopilotRange {
start: Position { start: CopilotPosition {
character: 0, character: 0,
line: position.line, line: position.line,
}, },
end: Position { end: CopilotPosition {
character: end_char, character: end_char,
line: position.line, line: position.line,
}, },
@ -63,17 +106,19 @@ impl CopilotCyclingCompletion {
} }
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct LanguageEntry { #[ts(export)]
language_id: String, pub struct LanguageEntry {
pub language_id: String,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct EditorConfiguration { #[ts(export)]
disabled_languages: Vec<LanguageEntry>, pub struct EditorConfiguration {
enable_auto_completions: bool, pub disabled_languages: Vec<LanguageEntry>,
pub enable_auto_completions: bool,
} }
impl Default for EditorConfiguration { impl Default for EditorConfiguration {
@ -84,47 +129,77 @@ impl Default for EditorConfiguration {
} }
} }
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)] #[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct EditorInfo { #[ts(export)]
name: String, pub struct EditorInfo {
version: String, pub name: String,
pub version: String,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)] #[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotEditorInfo { pub struct CopilotEditorInfo {
editor_configuration: EditorConfiguration, pub editor_configuration: EditorConfiguration,
editor_info: EditorInfo, pub editor_info: EditorInfo,
editor_plugin_info: EditorInfo, pub editor_plugin_info: EditorInfo,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DocParams { pub struct DocParams {
#[serde(skip)]
pub rope: Rope, pub rope: Rope,
pub uri: String, pub uri: String,
pub pos: Position, pub pos: CopilotPosition,
pub language: String, pub language: String,
pub line_before: String, pub line_before: String,
pub prefix: String, pub prefix: String,
pub suffix: String, pub suffix: String,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)] #[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotLspCompletionParams { pub struct CopilotLspCompletionParams {
pub doc: CopilotDocParams, pub doc: CopilotDocParams,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)] #[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotDocParams { pub struct CopilotDocParams {
pub indent_size: u32, pub indent_size: u32,
pub insert_spaces: bool, pub insert_spaces: bool,
pub language_id: String, pub language_id: String,
pub path: String, pub path: String,
pub position: Position, pub position: CopilotPosition,
pub relative_path: String, pub relative_path: String,
pub source: String, pub source: String,
pub tab_size: u32, pub tab_size: u32,
pub uri: String, pub uri: String,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotAcceptCompletionParams {
pub uuid: uuid::Uuid,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotRejectCompletionParams {
pub uuids: Vec<uuid::Uuid>,
}
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotCompletionTelemetry {
pub completion: CopilotCyclingCompletion,
pub params: CopilotLspCompletionParams,
}

View File

@ -1,11 +1,12 @@
//! Functions for the `kcl` lsp server. //! Functions for the `kcl` lsp server.
use std::collections::HashMap; use std::{collections::HashMap, io::Write, str::FromStr};
use anyhow::Result; use anyhow::Result;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
use clap::Parser; use clap::Parser;
use dashmap::DashMap; use dashmap::DashMap;
use sha2::Digest;
use tower_lsp::{ use tower_lsp::{
jsonrpc::Result as RpcResult, jsonrpc::Result as RpcResult,
lsp_types::{ lsp_types::{
@ -44,6 +45,7 @@ pub struct Server {
} }
/// The lsp server backend. /// The lsp server backend.
#[derive(Clone)]
pub struct Backend { pub struct Backend {
/// The client for the backend. /// The client for the backend.
pub client: Client, pub client: Client,
@ -285,6 +287,87 @@ impl Backend {
completions completions
} }
pub fn create_zip(&self) -> Result<Vec<u8>> {
// Collect all the file data we know.
let mut buf = vec![];
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
for entry in self.current_code_map.iter() {
let file_name = entry.key().replace("file://", "").to_string();
let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file(file_name, options)?;
zip.write_all(entry.value())?;
}
// Apply the changes you've made.
// Dropping the `ZipWriter` will have the same effect, but may silently fail
zip.finish()?;
drop(zip);
Ok(buf)
}
pub async fn send_telemetry(&self) -> Result<()> {
// Get information about the user.
let user = self
.zoo_client
.users()
.get_self()
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
// Hash the user's id.
// Create a SHA-256 object
let mut hasher = sha2::Sha256::new();
// Write input message
hasher.update(user.id);
// Read hash digest and consume hasher
let result = hasher.finalize();
// Get the hash as a string.
let user_id_hash = format!("{:x}", result);
// Get the workspace folders.
// The key of the workspace folder is the project name.
let workspace_folders = self.workspace_folders();
let project_names: Vec<String> = workspace_folders.iter().map(|v| v.name.clone()).collect::<Vec<_>>();
// Get the first name.
let project_name = project_names
.first()
.ok_or_else(|| anyhow::anyhow!("no project names"))?
.to_string();
// Send the telemetry data.
self.zoo_client
.meta()
.create_event(
vec![kittycad::types::multipart::Attachment {
// Clean the URI part.
name: "attachment".to_string(),
filename: Some("attachment.zip".to_string()),
content_type: Some("application/x-zip".to_string()),
data: self.create_zip()?,
}],
&kittycad::types::Event {
// This gets generated server side so leave empty for now.
attachment_uri: None,
created_at: chrono::Utc::now(),
event_type: kittycad::types::ModelingAppEventType::SuccessfulCompileBeforeClose,
last_compiled_at: Some(chrono::Utc::now()),
// We do not have project descriptions yet.
project_description: None,
project_name,
// The UUID for the modeling app.
// We can unwrap here because we know it will not panic.
source_id: uuid::Uuid::from_str("70178592-dfca-47b3-bd2d-6fce2bcaee04").unwrap(),
type_: kittycad::types::Type::ModelingAppEvent,
user_id: user_id_hash,
},
)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
Ok(())
}
} }
#[tower_lsp::async_trait] #[tower_lsp::async_trait]
@ -402,7 +485,32 @@ impl LanguageServer for Backend {
} }
async fn did_close(&self, params: DidCloseTextDocumentParams) { async fn did_close(&self, params: DidCloseTextDocumentParams) {
self.do_did_close(params).await self.do_did_close(params).await;
// Inject telemetry if we can train on the user's code.
// Return early if we cannot.
if !self.can_send_telemetry {
return;
}
// In wasm this needs to be spawn_local since fucking reqwests doesn't implement Send for wasm.
#[cfg(target_arch = "wasm32")]
{
let be = self.clone();
wasm_bindgen_futures::spawn_local(async move {
if let Err(err) = be.send_telemetry().await {
be.client
.log_message(MessageType::WARNING, format!("failed to send telemetry: {}", err))
.await;
}
});
}
#[cfg(not(target_arch = "wasm32"))]
if let Err(err) = self.send_telemetry().await {
self.client
.log_message(MessageType::WARNING, format!("failed to send telemetry: {}", err))
.await;
}
} }
async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> { async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> {

View File

@ -3,4 +3,6 @@
mod backend; mod backend;
pub mod copilot; pub mod copilot;
pub mod kcl; pub mod kcl;
#[cfg(test)]
mod tests;
mod util; mod util;

File diff suppressed because it is too large Load Diff

View File

@ -270,7 +270,8 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool)
workspace_folders: Default::default(), workspace_folders: Default::default(),
current_code_map: Default::default(), current_code_map: Default::default(),
editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())), editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())),
cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(), cache: Arc::new(kcl_lib::lsp::copilot::cache::CopilotCache::new()),
telemetry: Default::default(),
zoo_client, zoo_client,
}) })
.custom_method("setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info) .custom_method("setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info)
@ -278,7 +279,7 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool)
"getCompletions", "getCompletions",
kcl_lib::lsp::copilot::Backend::get_completions_cycling, kcl_lib::lsp::copilot::Backend::get_completions_cycling,
) )
.custom_method("notifyAccepted", kcl_lib::lsp::copilot::Backend::accept_completions) .custom_method("notifyAccepted", kcl_lib::lsp::copilot::Backend::accept_completion)
.custom_method("notifyRejected", kcl_lib::lsp::copilot::Backend::reject_completions) .custom_method("notifyRejected", kcl_lib::lsp::copilot::Backend::reject_completions)
.finish(); .finish();

View File

@ -28,7 +28,7 @@ async fn execute_and_snapshot(code: &str, units: kittycad::types::UnitLength) ->
let ws = client let ws = client
.modeling() .modeling()
.commands_ws(None, None, None, None, Some(false)) .commands_ws(None, None, None, None, None, Some(false))
.await?; .await?;
// Create a temporary file to write the output to. // Create a temporary file to write the output to.

View File

@ -30,7 +30,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
let ws = client let ws = client
.modeling() .modeling()
.commands_ws(None, None, None, None, Some(false)) .commands_ws(None, None, None, None, None, Some(false))
.await?; .await?;
let tokens = kcl_lib::token::lexer(code); let tokens = kcl_lib::token::lexer(code);